sanitize: block remote resource loads in feed bodies; tighten CSP
Rendering a feed item auto-fetched any remote <img> src, beaconing the
reader's IP to the feed publisher -- a tracking-pixel leak at odds with
the privacy pitch. The sanitizer now strips remote (http/https/protocol-
relative) src/srcset/poster while leaving click-only href intact. Also
drop script-src 'unsafe-inline' from the CSP; all scripts are external
files, no inline blocks or on* handlers exist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 files changed,
+19 insertions,
-4 deletions
| 76 |
76 |
|
continue;
|
| 77 |
77 |
|
}
|
| 78 |
78 |
|
|
| 79 |
|
- |
// Strip dangerous URL schemes in href and src.
|
| 80 |
|
- |
// Normalize by removing whitespace/control chars to defeat obfuscation.
|
| 81 |
|
- |
if (lower === 'href' || lower === 'src') {
|
|
79 |
+ |
// Auto-loading resource attributes (src/srcset/poster) fetch on
|
|
80 |
+ |
// render with no user action, so a remote URL here beacons the
|
|
81 |
+ |
// reader's IP to the feed publisher (tracking pixel). Strip both
|
|
82 |
+ |
// dangerous schemes and any remote (http/https/protocol-relative)
|
|
83 |
+ |
// target. Normalize away whitespace/control chars to defeat
|
|
84 |
+ |
// obfuscation. Relative URLs resolve against the app origin and
|
|
85 |
+ |
// don't leak, so they're left intact.
|
|
86 |
+ |
if (lower === 'src' || lower === 'srcset' || lower === 'poster') {
|
|
87 |
+ |
const value = (el.getAttribute(name) || '').replace(/[\s\x00-\x1f]/g, '').toLowerCase();
|
|
88 |
+ |
if (value.startsWith('javascript:') || value.startsWith('data:') || value.startsWith('vbscript:') ||
|
|
89 |
+ |
value.startsWith('http:') || value.startsWith('https:') || value.startsWith('//')) {
|
|
90 |
+ |
el.removeAttribute(name);
|
|
91 |
+ |
}
|
|
92 |
+ |
continue;
|
|
93 |
+ |
}
|
|
94 |
+ |
|
|
95 |
+ |
// Strip dangerous URL schemes in href (links only load on click).
|
|
96 |
+ |
if (lower === 'href') {
|
| 82 |
97 |
|
const value = (el.getAttribute(name) || '').replace(/[\s\x00-\x1f]/g, '').toLowerCase();
|
| 83 |
98 |
|
if (value.startsWith('javascript:') || value.startsWith('data:') || value.startsWith('vbscript:')) {
|
| 84 |
99 |
|
el.removeAttribute(name);
|
| 21 |
21 |
|
}
|
| 22 |
22 |
|
],
|
| 23 |
23 |
|
"security": {
|
| 24 |
|
- |
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src ipc: http://ipc.localhost http://127.0.0.1"
|
|
24 |
+ |
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src ipc: http://ipc.localhost http://127.0.0.1"
|
| 25 |
25 |
|
}
|
| 26 |
26 |
|
},
|
| 27 |
27 |
|
"bundle": {
|