aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-15 18:20:33 +0200
committerDanilo M. <danix@danix.xyz>2026-09-15 18:20:33 +0200
commit9c65aeb157fe16ab5a651f39c92902440f2d5344 (patch)
treec07d216503c4d278b318d61987bb1998d2bd671b
parent29886e00273127c228054e2b0097e12fe132c07f (diff)
downloadquickshell-9c65aeb157fe16ab5a651f39c92902440f2d5344.tar.gz
quickshell-9c65aeb157fe16ab5a651f39c92902440f2d5344.zip
fix(notifications): check every img src and uncache previewHEADmaster
The sanitizer matched the first \bsrc anywhere in the tag, so a data-src="file:///tmp/decoy" before a remote src won the match and kept the whole tag while Qt, which ignores data-src, fetched the remote image. Scan every src assignment in the tag and keep it only if all of them are local. Also set cache: false on the balloon preview, since the daemon overwrites the same path on a replace and QQuickPixmapCache keys on URL, so a replaced notification could show the previous image.
-rw-r--r--notifications/NotificationBalloon.qml1
-rw-r--r--shared/Notify.qml20
2 files changed, 14 insertions, 7 deletions
diff --git a/notifications/NotificationBalloon.qml b/notifications/NotificationBalloon.qml
index bfccda3..49ec0b4 100644
--- a/notifications/NotificationBalloon.qml
+++ b/notifications/NotificationBalloon.qml
@@ -155,5 +155,6 @@ Rectangle {
source: visible ? "file://" + b.notification.image : ""
fillMode: Image.PreserveAspectFit
asynchronous: true
+ cache: false
}
}
diff --git a/shared/Notify.qml b/shared/Notify.qml
index 237d8c9..1abe57d 100644
--- a/shared/Notify.qml
+++ b/shared/Notify.qml
@@ -70,9 +70,11 @@ Singleton {
// 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.
+ // default: a tag survives only if every src it carries decodes to a file:
+ // URL or a leading-slash absolute path. Every src assignment is checked,
+ // not just the first, so a decoy attribute cannot shadow a remote one;
+ // entities are decoded before the test, so an encoded scheme cannot slip
+ // past.
function sanitize(body) {
function decode(s) {
return s.replace(/&(?:#x([0-9a-f]+)|#(\d+)|(amp|colon|sol|tab|quot));/gi,
@@ -83,10 +85,14 @@ Singleton {
});
}
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 : "";
+ const re = /(?:^|\s)src\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
+ let m, found = false, local = true;
+ while ((m = re.exec(tag))) {
+ found = true;
+ const src = decode(m[1] !== undefined ? m[1] : m[2] !== undefined ? m[2] : m[3]);
+ if (!(/^file:/i.test(src) || (src.charAt(0) === "/" && src.charAt(1) !== "/"))) local = false;
+ }
+ return (found && local) ? tag : "";
});
}