diff options
| -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"]); } |
