fix(security): strip control characters before scanning CSS for url()
Review follow-up on the sanitizer dedup. sanitizeCSS already carried the rule — "any pass that can join tokens has to happen before validation, not after" — written above the URL scan to explain why it runs after the HTML-comment strip. The control-character strip is exactly such a pass and sat eleven lines below it. So `u<CTRL>rl(https://tracker.example/p.gif)` was scanned as clean, and the strip below then joined it into a live remote request with no warning. Newlines are control characters here too, so `u\nrl(...)` did it without an exotic byte. Verified against the real function before and after: all five variants returned a live remote url() and now return `none` plus the blocked-URL warning. This PR is what exposed it. Dropping newsletterService's second stripRemoteCssUrls pass was right — the duplicate hid a defect in the shared sanitizer rather than fixing it — but it removed the belt that was catching this for the newsletter path. Fixing the ordering fixes it for every caller instead of restoring the second pass. Refs #1264
This commit is contained in:
@@ -221,6 +221,24 @@ describe('sanitizeCSS', () => {
|
||||
expect(asParsed(sanitized)).not.toContain('evil.example');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a C0 control character', '\u0001'],
|
||||
['a NUL byte', '\u0000'],
|
||||
['a DEL byte', '\u007F'],
|
||||
// Newlines are control characters for this strip, so the bypass did not
|
||||
// need an exotic byte — ordinary-looking wrapped CSS was enough.
|
||||
['a newline', '\n'],
|
||||
])('blocks a url() that only becomes one after %s is removed', (_label, ch) => {
|
||||
// Same token-joining hazard as the HTML-comment case above: the control
|
||||
// strip used to run AFTER the URL scan, so `u\u0001rl(...)` was scanned
|
||||
// as clean and then joined into a live remote request, with no warning.
|
||||
const { sanitized, warnings } = sanitizeCSS(
|
||||
`.a{background:u${ch}rl(https://evil.example/p.gif)}`
|
||||
);
|
||||
expect(asParsed(sanitized)).not.toContain('evil.example');
|
||||
expect(warnings.join(' ')).toContain('external URL');
|
||||
});
|
||||
|
||||
it('still blocks the other forbidden patterns', () => {
|
||||
const { sanitized } = sanitizeCSS(
|
||||
'@import url("https://x/e.css"); .a{width:expression(alert(1));behavior:url(e.htc)}'
|
||||
|
||||
Reference in New Issue
Block a user