Skip to content

Commit 6d5690e

Browse files
committed
refactor: update stylesheet sanitization logic
1 parent 3f37330 commit 6d5690e

2 files changed

Lines changed: 94 additions & 68 deletions

File tree

__tests__/sanitize.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,36 @@ b {background: red;}</style>`);
368368
'<div id="test"><span style="background: url(./redirect?url=./image.jpg);"></span></div>',
369369
);
370370
});
371+
372+
it('handles new CSS rules', () => {
373+
expect(
374+
sanitize(
375+
`<style>
376+
@namespace url(http://www.w3.org/1999/xhtml);
377+
@import url(https://fonts.googleapis.com/css2?family=Bungee+Spice);
378+
@font-face {
379+
font-family: "Open Sans";
380+
src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"),
381+
url("/fonts/OpenSans-Regular-webfont.woff") format("woff");
382+
}
383+
@container (width < 650px) {
384+
.test { color: red; }
385+
}
386+
@supports (display: grid) {
387+
.test { color: red; }
388+
}
389+
@keyframes example {
390+
from {
391+
color: green;
392+
}
393+
to {
394+
color: red;
395+
}
396+
}
397+
</style>`,
398+
'',
399+
{ noWrapper: true, preserveCssPriority: false },
400+
),
401+
).toBe(`<style></style>`);
402+
});
371403
});

src/index.ts

Lines changed: 62 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -182,23 +182,9 @@ function sanitizeHtml(
182182
const disallowedList = doc.querySelectorAll(removeTags.join(', '));
183183
disallowedList.forEach(element => element.remove());
184184

185-
// Move styles from head to body.
186-
const styleList = doc.querySelectorAll('head > style');
187-
styleList.forEach(element => {
188-
doc.body.appendChild(element);
189-
});
190-
191185
// Filter other tags.
192186
const toRemove: Element[] = [];
193-
const elementIter = doc.createNodeIterator(
194-
doc.body,
195-
NodeFilter.SHOW_ELEMENT,
196-
{
197-
acceptNode: () => {
198-
return NodeFilter.FILTER_ACCEPT;
199-
},
200-
},
201-
);
187+
const elementIter = doc.createNodeIterator(doc.body, NodeFilter.SHOW_ELEMENT);
202188

203189
while ((node = elementIter.nextNode())) {
204190
const element = node as HTMLElement;
@@ -287,67 +273,75 @@ function sanitizeHtml(
287273
}
288274
}
289275

290-
// Prepend wrapper ID.
291-
const bodyStyleList = doc.querySelectorAll('body style');
292-
bodyStyleList.forEach(element => {
293-
const styleElement = element as HTMLStyleElement;
294-
const stylesheet = styleElement.sheet as CSSStyleSheet;
276+
const styleList = doc.querySelectorAll('style');
277+
278+
if (styleList.length) {
279+
const sanitizedStyle = doc.createElement('style');
280+
281+
doc.body.append(sanitizedStyle);
282+
283+
const sheet = sanitizedStyle.sheet!;
295284
const newRules: CSSRule[] = [];
296285

297-
if (!stylesheet.cssRules) {
298-
styleElement.textContent = '';
299-
return;
300-
}
286+
styleList.forEach(element => {
287+
const styleElement = element as HTMLStyleElement;
288+
const stylesheet = styleElement.sheet as CSSStyleSheet;
301289

302-
for (let i = 0; i < stylesheet.cssRules.length; i++) {
303-
const rule = stylesheet.cssRules[i] as CSSStyleRule;
304-
305-
if ('selectorText' in rule) {
306-
sanitizeCssRule(
307-
rule,
308-
id,
309-
allowedSchemas,
310-
allowedCssProperties,
311-
preserveCssPriority,
312-
rewriteExternalResources,
313-
);
314-
newRules.push(rule);
315-
} else if ('cssRules' in rule && 'media' in rule) {
316-
// According to https://www.caniemail.com/,
317-
// out of all at-rules, Gmail only supports @media.
318-
const mediaRule = rule as any as CSSMediaRule;
319-
const newRulesMedia: CSSRule[] = [];
320-
321-
for (let i = 0; i < mediaRule.cssRules.length; i++) {
322-
const rule = mediaRule.cssRules[i] as CSSStyleRule;
323-
324-
if (rule.type === rule.STYLE_RULE) {
325-
sanitizeCssRule(
326-
rule,
327-
id,
328-
allowedSchemas,
329-
allowedCssProperties,
330-
preserveCssPriority,
331-
rewriteExternalResources,
332-
);
333-
newRulesMedia.push(rule);
334-
}
335-
}
290+
if (!stylesheet.cssRules) {
291+
styleElement.textContent = '';
292+
return;
293+
}
336294

337-
while (mediaRule.cssRules.length > 0) {
338-
mediaRule.deleteRule(0);
339-
}
295+
for (let i = 0; i < stylesheet.cssRules.length; i++) {
296+
const rule = stylesheet.cssRules[i];
297+
298+
if (rule instanceof CSSStyleRule) {
299+
sanitizeCssRule(
300+
rule,
301+
id,
302+
allowedSchemas,
303+
allowedCssProperties,
304+
preserveCssPriority,
305+
rewriteExternalResources,
306+
);
340307

341-
for (const rule of newRulesMedia) {
342-
mediaRule.insertRule(rule.cssText, mediaRule.cssRules.length);
343-
}
308+
newRules.push(rule);
309+
} else if (rule instanceof CSSMediaRule) {
310+
const idx = sheet.insertRule('@media {}', sheet.cssRules.length);
311+
const sanitizedMediaRule = sheet.cssRules[idx] as CSSMediaRule;
312+
313+
// According to https://www.caniemail.com/,
314+
// out of all at-rules, Gmail only supports @media.
315+
const mediaRule = rule as any as CSSMediaRule;
316+
317+
for (let i = 0; i < mediaRule.cssRules.length; i++) {
318+
const rule = mediaRule.cssRules[i];
319+
320+
if (rule instanceof CSSStyleRule) {
321+
sanitizeCssRule(
322+
rule,
323+
id,
324+
allowedSchemas,
325+
allowedCssProperties,
326+
preserveCssPriority,
327+
rewriteExternalResources,
328+
);
329+
sanitizedMediaRule.insertRule(
330+
rule.cssText,
331+
sanitizedMediaRule.cssRules.length,
332+
);
333+
}
334+
}
344335

345-
newRules.push(mediaRule);
336+
newRules.push(mediaRule);
337+
}
346338
}
347-
}
348339

349-
styleElement.textContent = newRules.map(rule => rule.cssText).join('\n');
350-
});
340+
styleElement.remove();
341+
});
342+
343+
sanitizedStyle.textContent = newRules.map(rule => rule.cssText).join('\n');
344+
}
351345

352346
// Wrap body inside of a div with the generated ID.
353347
if (noWrapper) {

0 commit comments

Comments
 (0)