diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-15 18:15:50 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-15 18:15:50 +0200 |
| commit | 29886e00273127c228054e2b0097e12fe132c07f (patch) | |
| tree | 3df7ba587a91d5daa6198c31033daed40272c59a /shared/Notify.qml | |
| parent | ea2c64c9a6fbc22bffbac4e2e57ce0762f5e03ad (diff) | |
| download | quickshell-29886e00273127c228054e2b0097e12fe132c07f.tar.gz quickshell-29886e00273127c228054e2b0097e12fe132c07f.zip | |
fix(notifications): allowlist local inline image sources
The http(s)-only regex let protocol-relative //host, ftp, data and
entity-encoded schemes through, and the comment overstated what it
removed. Replace it with a deny-by-default allowlist: an <img> is
removed unless its src, entities decoded first, is a file: URL or a
single leading slash. Protocol-relative //host is rejected while a
/absolute/path is kept.
Diffstat (limited to 'shared/Notify.qml')
| -rw-r--r-- | shared/Notify.qml | 27 |
1 files changed, 22 insertions, 5 deletions
diff --git a/shared/Notify.qml b/shared/Notify.qml index c79f5f5..237d8c9 100644 --- a/shared/Notify.qml +++ b/shared/Notify.qml @@ -66,13 +66,30 @@ Singleton { function close(id) { root.run(["close", String(id)]); } function closeAll() { root.run(["close-all"]); } - // Inline images are local only. A notification is untrusted input, and a - // remote <img src> would otherwise make the shell fetch a URL, which leaks - // that the notification was shown. This removes such tags before the - // RichText body renders; a local path or file:// source is left alone. + // Inline images are local only. A notification is untrusted input, and any + // remote <img src> (http(s), protocol-relative //host, ftp, data, or an + // entity-encoded scheme) would make the shell fetch or embed something the + // sender chose, which leaks that the notification was shown. Deny by + // default: a tag survives only if its src decodes to a file: URL or a + // leading-slash absolute path. Entities are decoded before the test, so an + // encoded scheme cannot slip past. function sanitize(body) { - return (body || "").replace(/<img\b[^>]*\bsrc\s*=\s*["']?\s*https?:\/\/[^>]*>/gi, ""); + function decode(s) { + return s.replace(/&(?:#x([0-9a-f]+)|#(\d+)|(amp|colon|sol|tab|quot));/gi, + function (m, hex, dec, name) { + if (hex !== undefined) return String.fromCharCode(parseInt(hex, 16)); + if (dec !== undefined) return String.fromCharCode(parseInt(dec, 10)); + return { amp: "&", colon: ":", sol: "/", tab: "\t", quot: "\"" }[name.toLowerCase()]; + }); + } + return (body || "").replace(/<img\b[^>]*>/gi, function (tag) { + const m = tag.match(/\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i); + if (!m) return ""; + const src = decode(m[1] !== undefined ? m[1] : m[2] !== undefined ? m[2] : m[3]); + return /^file:/i.test(src) || (src.charAt(0) === "/" && src.charAt(1) !== "/") ? tag : ""; + }); } + function action(id, key) { root.run(["action", String(id), key]); } function clearHistory() { root.run(["clear-history"]); } |
