aboutsummaryrefslogtreecommitdiffstats
path: root/shared/Notify.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-15 18:15:50 +0200
committerDanilo M. <danix@danix.xyz>2026-09-15 18:15:50 +0200
commit29886e00273127c228054e2b0097e12fe132c07f (patch)
tree3df7ba587a91d5daa6198c31033daed40272c59a /shared/Notify.qml
parentea2c64c9a6fbc22bffbac4e2e57ce0762f5e03ad (diff)
downloadquickshell-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.qml27
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"]); }