feat: strip external url()/@import from <style> blocks in sanitizer (#457)
Defence-in-depth on top of the strict iframe img-src/media-src/font-src CSP that already blocks <style>-tag fetches at the network level. The per-node DOM walk in blockExternalResourcesOnNode only sees element attributes, so a tracker hidden in a kept <style> block (background url(), @font-face, @import) never passed through it. Adds stripExternalStyleSheetCss(), wired into blockExternalResourcesOnNode for STYLE nodes (so it's gated on shouldBlockExternal and drives the blocked-content banner like every other vector). Decodes CSS escapes over the whole block first so the escaped-keyword form \75\72\6C( -> url( is caught - a literal `url(` match would miss it. Removes remote @import in both url() and bare-string forms.
This commit is contained in:
committed by
Linus Rath
parent
b4739c111f
commit
abd493fb4c
@@ -14,6 +14,7 @@ import {
|
||||
decodeCssEscapes,
|
||||
styleHasExternalUrl,
|
||||
stripExternalCssUrls,
|
||||
stripExternalStyleSheetCss,
|
||||
blockExternalResourcesOnNode,
|
||||
TRANSPARENT_BLOCKED_PIXEL,
|
||||
} from '../email-sanitization';
|
||||
@@ -396,6 +397,42 @@ describe('email-sanitization', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripExternalStyleSheetCss (<style> block defence-in-depth, #457)', () => {
|
||||
it('strips external url() inside a style rule', () => {
|
||||
expect(stripExternalStyleSheetCss("#x{background:url('http://tracker.example/y')}"))
|
||||
.toBe('#x{background:url()}');
|
||||
});
|
||||
|
||||
it('strips the CSS-escaped url() keyword (\\75\\72\\6C()', () => {
|
||||
expect(stripExternalStyleSheetCss("#x{background:\\75\\72\\6C('http://tracker.example/y')}"))
|
||||
.toBe('#x{background:url()}');
|
||||
});
|
||||
|
||||
it('removes a remote @import (bare-string form)', () => {
|
||||
expect(stripExternalStyleSheetCss('@import "http://tracker.example/s.css";\n#x{color:red}'))
|
||||
.toBe('\n#x{color:red}');
|
||||
expect(stripExternalStyleSheetCss("@import '//tracker.example/s.css';")).toBe('');
|
||||
});
|
||||
|
||||
it('neutralises a remote @import url() form', () => {
|
||||
expect(stripExternalStyleSheetCss('@import url(http://tracker.example/s.css);'))
|
||||
.toBe('@import url();');
|
||||
});
|
||||
|
||||
it('leaves a stylesheet with no external refs unchanged (escapes intact)', () => {
|
||||
const css = '#x{content:"\\2014";background:url(data:image/png;base64,AAAA)}';
|
||||
expect(stripExternalStyleSheetCss(css)).toBe(css);
|
||||
});
|
||||
|
||||
it('blockExternalResourcesOnNode strips a <style> block and reports blocked', () => {
|
||||
const style = parseHtmlSafely(
|
||||
'<body><style>#x{background:url(http://tracker.example/y)}</style></body>',
|
||||
).body.firstElementChild!;
|
||||
expect(blockExternalResourcesOnNode(style)).toBe(true);
|
||||
expect(style.textContent).toBe('#x{background:url()}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('blockExternalResourcesOnNode (anti-tracking vectors)', () => {
|
||||
function el(html: string): Element {
|
||||
return parseHtmlSafely(`<body>${html}</body>`).body.firstElementChild!;
|
||||
|
||||
Reference in New Issue
Block a user