Low: Fix self-XSS in SVGs

This commit is contained in:
binwiederhier 2026-05-17 09:57:41 -04:00
parent 160c916ce0
commit 29113402ce
5 changed files with 22 additions and 15 deletions

View file

@ -1898,6 +1898,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
**Bug fixes + maintenance:**
* Remove `stacktrace-js`, `stacktrace-gps`, `humanize-duration`, and `js-base64` from the web app to reduce dependency and security footprint
* Restrict the publish dialog's local file preview to safe image types (png/jpg/gif/webp) to prevent same-origin script execution from blob URLs when previewing a crafted SVG ([GHSA-j8hr-p342-xrmh](https://github.com/binwiederhier/ntfy/security/advisories/GHSA-j8hr-p342-xrmh), thanks to [@Venukamatchi](https://github.com/Venukamatchi) for reporting)
## ntfy Android v1.25.x (UNRELEASED)

View file

@ -35,7 +35,7 @@ export const formatMessage = (m) => {
return m.message || "";
};
const imageRegex = /\.(png|jpe?g|gif|webp)$/i;
export const imageRegex = /\.(png|jpe?g|gif|webp)$/i;
export const isImage = (attachment) => {
if (!attachment) return false;

View file

@ -145,8 +145,7 @@ export const hashCode = (s) => {
* which is expected by `<html lang>` and `Intl.DateTimeFormat`. Falls back to "en"
* if the input is missing or not a string.
*/
export const getKebabCaseLangStr = (language) =>
typeof language === "string" && language.length > 0 ? language.replace(/_/g, "-") : "en";
export const getKebabCaseLangStr = (language) => (typeof language === "string" && language.length > 0 ? language.replace(/_/g, "-") : "en");
export const formatShortDateTime = (timestamp, language) =>
new Intl.DateTimeFormat(getKebabCaseLangStr(language), {

View file

@ -31,18 +31,24 @@ const AttachmentIcon = (props) => {
imageFile = fileDocument;
imageLabel = t("notifications_attachment_file_document");
}
const icon = (
<Box
component="img"
src={imageFile}
alt={imageLabel}
loading="lazy"
sx={{
width: "28px",
height: "28px",
}}
/>
);
if (!props.href) {
return icon;
}
return (
<Link href={props.href} target="_blank">
<Box
component="img"
src={imageFile}
alt={imageLabel}
loading="lazy"
sx={{
width: "28px",
height: "28px",
}}
/>
<Link href={props.href} target="_blank" rel="noopener noreferrer">
{icon}
</Link>
);
};

View file

@ -30,6 +30,7 @@ import priority3 from "../img/priority-3.svg";
import priority4 from "../img/priority-4.svg";
import priority5 from "../img/priority-5.svg";
import { formatBytes, maybeWithAuth, topicShortUrl, topicUrl, validTopic, validUrl } from "../app/utils";
import { imageRegex } from "../app/notificationUtils";
import AttachmentIcon from "./AttachmentIcon";
import DialogFooter from "./DialogFooter";
import api from "../app/Api";
@ -805,7 +806,7 @@ const AttachmentBox = (props) => {
borderRadius: "4px",
}}
>
<AttachmentIcon type={file.type} href={URL.createObjectURL(file)} />
<AttachmentIcon type={file.type} href={imageRegex.test(file.name) ? URL.createObjectURL(file) : undefined} />
<Box sx={{ marginLeft: 1, textAlign: "left" }}>
<ExpandingTextField
minWidth={140}