Closed JeremyBradshaw7 closed 8 months ago
Hi @JeremyBradshaw7,
Sorry for the delay in getting back to you.
There are two issues with your sanitize object.
First, you are correct that you can't (as far as I can see) use a wildcard for the CSS properties within the allowedStyles
. The only solution that I can see, which would have potential performance impact, is to run all tags through 'transformTagsand perform your regex matching on the style attribute in that function to strip dangerous content. You would also set
parseStyleAttributes: falsesince you would be parsing them "manually". Second, your current approach would only parse the styling within the style attribute. Anything could be added into the style tag. To filter that I would use an
exclusiveFilter` function.
exclusiveFilter: function (frame) {
if (frame.tag === 'style') {
const regex = /(expression|script|behavior|url)/i;
return regex.test(frame.text);
}
return false;
}
Basically, if someone tries to sneak those words into a style tag you chuck the whole thing.
The only other thought that I have about your general approach is that I would use a lot of caution. I'm not an expert with regard to XSS or other attacks, but I don't think using regex to look for a few words will be highly effective. Just my 2 cents. I would be interested in other's responses.
Cheers, Bob
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Based on what @JeremyBradshaw7 has done, I came up with something like this:
import sanitizeHtml from 'sanitize-html';
const cssAttributes = [
'accent-color',
'align-content',
'align-items',
'align-self',
'all',
'animation',
'animation-delay',
'animation-direction',
'animation-duration',
'animation-fill-mode',
'animation-iteration-count',
'animation-name',
'animation-play-state',
'animation-timing-function',
'aspect-ratio',
'backdrop-filter',
'backface-visibility',
'background',
'background-attachment',
'background-blend-mode',
'background-clip',
'background-color',
'background-image',
'background-origin',
'background-position',
'background-position-x',
'background-position-y',
'background-repeat',
'background-size',
'block-size',
'border',
'border-block',
'border-block-color',
'border-block-end',
'border-block-end-color',
'border-block-end-style',
'border-block-end-width',
'border-block-start',
'border-block-start-color',
'border-block-start-style',
'border-block-start-width',
'border-block-style',
'border-block-width',
'border-bottom',
'border-bottom-color',
'border-bottom-left-radius',
'border-bottom-right-radius',
'border-bottom-style',
'border-bottom-width',
'border-collapse',
'border-color',
'border-end-end-radius',
'border-end-start-radius',
'border-image',
'border-image-outset',
'border-image-repeat',
'border-image-slice',
'border-image-source',
'border-image-width',
'border-inline',
'border-inline-color',
'border-inline-end',
'border-inline-end-color',
'border-inline-end-style',
'border-inline-end-width',
'border-inline-start',
'border-inline-start-color',
'border-inline-start-style',
'border-inline-start-width',
'border-inline-style',
'border-inline-width',
'border-left',
'border-left-color',
'border-left-style',
'border-left-width',
'border-radius',
'border-right',
'border-right-color',
'border-right-style',
'border-right-width',
'border-spacing',
'border-start-end-radius',
'border-start-start-radius',
'border-style',
'border-top',
'border-top-color',
'border-top-left-radius',
'border-top-right-radius',
'border-top-style',
'border-top-width',
'border-width',
'bottom',
'box-decoration-break',
'box-reflect',
'box-shadow',
'box-sizing',
'break-after',
'break-before',
'break-inside',
'caption-side',
'caret-color',
'@charset',
'clear',
'clip',
'clip-path',
'color',
'column-count',
'column-fill',
'column-gap',
'column-rule',
'column-rule-color',
'column-rule-style',
'column-rule-width',
'column-span',
'column-width',
'columns',
'content',
'counter-increment',
'counter-reset',
'counter-set',
'cursor',
'direction',
'display',
'empty-cells',
'filter',
'flex',
'flex-basis',
'flex-direction',
'flex-flow',
'flex-grow',
'flex-shrink',
'flex-wrap',
'float',
'font',
'@font-face',
'font-family',
'font-feature-settings',
'font-kerning',
'font-size',
'font-size-adjust',
'font-stretch',
'font-style',
'font-variant',
'font-variant-caps',
'font-weight',
'gap',
'grid',
'grid-area',
'grid-auto-columns',
'grid-auto-flow',
'grid-auto-rows',
'grid-column',
'grid-column-end',
'grid-column-gap',
'grid-column-start',
'grid-gap',
'grid-row',
'grid-row-end',
'grid-row-gap',
'grid-row-start',
'grid-template',
'grid-template-areas',
'grid-template-columns',
'grid-template-rows',
'hanging-punctuation',
'height',
'hyphens',
'hypenate-character',
'image-rendering',
'@import',
'inline-size',
'inset',
'inset-block',
'inset-block-end',
'inset-block-start',
'inset-inline',
'inset-inline-end',
'inset-inline-start',
'isolation',
'justify-content',
'justify-items',
'justify-self',
'@keyframes',
'left',
'letter-spacing',
'line-height',
'list-style',
'list-style-image',
'list-style-position',
'list-style-type',
'margin',
'margin-block',
'margin-block-end',
'margin-block-start',
'margin-bottom',
'margin-inline',
'margin-inline-end',
'margin-inline-start',
'margin-left',
'margin-right',
'margin-top',
'mask-image',
'mask-mode',
'mask-origin',
'mask-position',
'mask-repeat',
'mask-size',
'max-height',
'max-width',
'@media',
'max-block-size',
'max-inline-size',
'min-block-size',
'min-inline-size',
'min-height',
'min-width',
'mix-blend-mode',
'object-fit',
'object-position',
'offset',
'offset-anchor',
'offset-distance',
'offset-path',
'offset-rotate',
'opacity',
'order',
'orphans',
'outline',
'outline-color',
'outline-offset',
'outline-style',
'outline-width',
'overflow',
'overflow-anchor',
'overflow-wrap',
'overflow-x',
'overflow-y',
'overscroll-behavior',
'overscroll-behavior-block',
'overscroll-behavior-inline',
'overscroll-behavior-x',
'overscroll-behavior-y',
'padding',
'padding-block',
'padding-block-end',
'padding-block-start',
'padding-bottom',
'padding-inline',
'padding-inline-end',
'padding-inline-start',
'padding-left',
'padding-right',
'padding-top',
'page-break-after',
'page-break-before',
'page-break-inside',
'paint-order',
'perspective',
'perspective-origin',
'place-content',
'place-items',
'place-self',
'pointer-events',
'position',
'quotes',
'resize',
'right',
'rotate',
'row-gap',
'scale',
'scroll-behavior',
'scroll-margin',
'scroll-margin-block',
'scroll-margin-block-end',
'scroll-margin-block-start',
'scroll-margin-bottom',
'scroll-margin-inline',
'scroll-margin-inline-end',
'scroll-margin-inline-start',
'scroll-margin-left',
'scroll-margin-right',
'scroll-margin-top',
'scroll-padding',
'scroll-padding-block',
'scroll-padding-block-end',
'scroll-padding-block-start',
'scroll-padding-bottom',
'scroll-padding-inline',
'scroll-padding-inline-end',
'scroll-padding-inline-start',
'scroll-padding-left',
'scroll-padding-right',
'scroll-padding-top',
'scroll-snap-align',
'scroll-snap-stop',
'scroll-snap-type',
'scrollbar-color',
'tab-size',
'table-layout',
'text-align',
'text-align-last',
'text-decoration',
'text-decoration-color',
'text-decoration-line',
'text-decoration-style',
'text-decoration-thickness',
'text-emphasis',
'text-emphasis-color',
'text-emphasis-position',
'text-emphasis-style',
'text-indent',
'text-justify',
'text-orientation',
'text-overflow',
'text-shadow',
'text-transform',
'text-underline-offset',
'text-underline-position',
'top',
'transform',
'transform-origin',
'transform-style',
'transition',
'transition-delay',
'transition-duration',
'transition-property',
'transition-timing-function',
'translate',
'unicode-bidi',
'user-select',
'vertical-align',
'visibility',
'white-space',
'widows',
'width',
'word-break',
'word-spacing',
'word-wrap',
'writing-mode',
'z-index',
];
const allowedStyleRules = cssAttributes.reduce((accumulator, value) => {
return {
...accumulator,
[value]: [/^(?!.*(?:expression|script|behavior|url)).*$/i],
};
}, {});
const html = '<p style="vertical-align: middle;font-size:2em;">Hi</p>';
console.log(
sanitizeHtml(html, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'a']),
allowedSchemes: ['data', 'https', 'mailto', 'tel'],
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
'*': ['style'],
},
allowedStyles: {
'*': allowedStyleRules,
},
})
);
So yes, I grabbed the whole list of available CSS attributes here: https://www.w3schools.com/cssref/index.php
with let cssRules = []; document.querySelectorAll('td:first-child > a').forEach(node => cssRules.push(node.innerHTML));
.
I plan to allow style tags and attributes through, but I do want to strip out any security vulnerabilities from them, such as any
expression
orjavascript:
use as discussed here.How can that be achieved in sanitize-html?
I've tried this but it seems to be stripping out ALL styles:
Is it that at the style attribute level it does not support wildcards?