aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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"]); }