sanitize-html 2.3.2 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -1
- package/README.md +51 -9
- package/index.js +70 -11
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.1 (2021-09-14):
|
|
4
|
+
- The `allowedScriptHostnames` and `allowedScriptDomains` options now implicitly purge the inline content of all script tags, not just those with `src` attributes. This behavior was already strongly implied by the fact that they purged it in the case where a `src` attribute was actually present, and is necessary for the feature to provide any real security. Thanks to Grigorii Duca for pointing out the issue.
|
|
5
|
+
|
|
6
|
+
## 2.5.0 (2021-09-08):
|
|
7
|
+
|
|
8
|
+
- New `allowedScriptHostnames` option, it enables you to specify which hostnames are allowed in a script tag.
|
|
9
|
+
- New `allowedScriptDomains` option, it enables you to specify which domains are allowed in a script tag. Thank you to [Yorick Girard](https://github.com/yorickgirard) for this and the `allowedScriptHostnames` contribution.
|
|
10
|
+
- Updates whitelist to allowlist.
|
|
11
|
+
|
|
12
|
+
## 2.4.0 (2021-05-19):
|
|
13
|
+
- Added support for class names with wildcards in `allowedClasses`. Thanks to [zhangbenber](https://github.com/zhangbenber) for the contribution.
|
|
14
|
+
|
|
15
|
+
## 2.3.3 (2021-03-19):
|
|
16
|
+
- Security fix: `allowedSchemes` and related options did not properly block schemes containing a hyphen, plus sign, period or digit, such as `ms-calculator:`. Thanks to Lukas Euler for pointing out the issue.
|
|
17
|
+
- Added a security note about the known risks associated with using the `parser` option, especially `decodeEntities: false`. See the documentation.
|
|
18
|
+
|
|
3
19
|
## 2.3.2 (2021-01-26):
|
|
20
|
+
|
|
4
21
|
- Additional fixes for iframe validation exploits. Prevent exploits based on browsers' tolerance of the use of "\" rather than "/" and the presence of whitespace at this point in the URL. Thanks to Ron Masas of [Checkmarx](https://www.checkmarx.com/) for pointing out the issue and writing unit tests.
|
|
5
|
-
-
|
|
22
|
+
- Updates README `yarn add` syntax. Thanks to [Tagir Khadshiev](https://github.com/Aspedm) for the contribution.
|
|
6
23
|
|
|
7
24
|
## 2.3.1 (2021-01-22):
|
|
8
25
|
- Uses the standard WHATWG URL parser to stop IDNA (Internationalized Domain Name) attacks on the iframe hostname validator. Thanks to Ron Masas of [Checkmarx](https://www.checkmarx.com/) for pointing out the issue and suggesting the use of the WHATWG parser.
|
package/README.md
CHANGED
|
@@ -26,9 +26,19 @@ HTML comments are not preserved.
|
|
|
26
26
|
|
|
27
27
|
sanitize-html is intended for use with Node.js and supports Node 10+. All of its npm dependencies are pure JavaScript. sanitize-html is built on the excellent `htmlparser2` module.
|
|
28
28
|
|
|
29
|
-
### Regarding
|
|
29
|
+
### Regarding TypeScript
|
|
30
30
|
|
|
31
|
-
sanitize-html is not written in
|
|
31
|
+
sanitize-html is not written in TypeScript and there is no plan to directly support it. There is a community supported typing definition, [`@types/sanitize-html`](https://www.npmjs.com/package/@types/sanitize-html), however.
|
|
32
|
+
```bash
|
|
33
|
+
npm install -D @types/sanitize-html
|
|
34
|
+
```
|
|
35
|
+
If `esModuleInterop=true` is not set in your `tsconfig.json` file, you have to import it with:
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
import * as sanitizeHtml from 'sanitize-html';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Any questions or problems while using `@types/sanitize-html` should be directed to its maintainers as directed by that project's contribution guidelines.
|
|
32
42
|
|
|
33
43
|
## How to use
|
|
34
44
|
|
|
@@ -38,11 +48,14 @@ sanitize-html is not written in Typescript and there is no plan to directly supp
|
|
|
38
48
|
|
|
39
49
|
But, perhaps you'd like to display sanitized HTML immediately in the browser for preview. Or ask the browser to do the sanitization work on every page load. You can if you want to!
|
|
40
50
|
|
|
41
|
-
*
|
|
42
|
-
* Run npm install and:
|
|
51
|
+
* Install the package:
|
|
43
52
|
|
|
44
53
|
```bash
|
|
45
|
-
npm install sanitize-html
|
|
54
|
+
npm install sanitize-html
|
|
55
|
+
```
|
|
56
|
+
or
|
|
57
|
+
```
|
|
58
|
+
yarn add sanitize-html
|
|
46
59
|
```
|
|
47
60
|
|
|
48
61
|
The primary change in the 2.x version of sanitize-html is that it no longer includes a build that is ready for browser use. Developers are expected to include sanitize-html in their project builds (e.g., webpack) as they would any other dependency. So while sanitize-html is no longer ready to link to directly in HTML, developers can now more easily process it according to their needs.
|
|
@@ -69,7 +82,7 @@ npm install sanitize-html
|
|
|
69
82
|
|
|
70
83
|
Import the module:
|
|
71
84
|
|
|
72
|
-
```
|
|
85
|
+
```js
|
|
73
86
|
// In ES modules
|
|
74
87
|
import sanitizeHtml from 'sanitize-html';
|
|
75
88
|
|
|
@@ -224,16 +237,17 @@ const clean = sanitizeHtml(dirty, {
|
|
|
224
237
|
});
|
|
225
238
|
```
|
|
226
239
|
|
|
227
|
-
Similar to `allowedAttributes`, you can use `*` as a tag name
|
|
240
|
+
Similar to `allowedAttributes`, you can use `*` to allow classes with a certain prefix, or use `*` as a tag name to allow listed classes to be valid for any tag:
|
|
228
241
|
```js
|
|
229
242
|
allowedClasses: {
|
|
243
|
+
'code': [ 'language-*', 'lang-*' ],
|
|
230
244
|
'*': [ 'fancy', 'simple' ]
|
|
231
245
|
}
|
|
232
246
|
```
|
|
233
247
|
|
|
234
248
|
### Allowed CSS Styles
|
|
235
249
|
|
|
236
|
-
If you wish to allow specific CSS _styles_ on a particular element, you can do that with the `allowedStyles` option. Simply declare your desired attributes as regular expression options within an array for the given attribute. Specific elements will inherit
|
|
250
|
+
If you wish to allow specific CSS _styles_ on a particular element, you can do that with the `allowedStyles` option. Simply declare your desired attributes as regular expression options within an array for the given attribute. Specific elements will inherit allowlisted attributes from the global (`*`) attribute. Any other CSS classes are discarded.
|
|
237
251
|
|
|
238
252
|
**You must also use `allowedAttributes`** to activate the `style` attribute for the relevant elements. Otherwise this feature will never come into play.
|
|
239
253
|
|
|
@@ -274,7 +288,9 @@ enforceHtmlBoundary: true
|
|
|
274
288
|
|
|
275
289
|
### htmlparser2 Options
|
|
276
290
|
|
|
277
|
-
sanitize-html is built on `htmlparser2`. By default the only option passed down is `decodeEntities: true
|
|
291
|
+
sanitize-html is built on `htmlparser2`. By default the only option passed down is `decodeEntities: true`. You can set the options to pass by using the parser option.
|
|
292
|
+
|
|
293
|
+
**Security note: changing the `parser` settings can be risky.** In particular, `decodeEntities: false` has known security concerns and a complete test suite does not exist for every possible combination of settings when used with `sanitize-html`. If security is your goal we recommend you use the defaults rather than changing `parser`, except for the `lowerCaseTags` option.
|
|
278
294
|
|
|
279
295
|
```javascript
|
|
280
296
|
const clean = sanitizeHtml(dirty, {
|
|
@@ -500,6 +516,32 @@ const clean = sanitizeHtml('<p><iframe src="https://us02web.zoom.us/embed/12345"
|
|
|
500
516
|
});
|
|
501
517
|
```
|
|
502
518
|
|
|
519
|
+
### Script Filters
|
|
520
|
+
|
|
521
|
+
Similarly to iframes you can allow a script tag on a list of allowlisted domains
|
|
522
|
+
|
|
523
|
+
```js
|
|
524
|
+
const clean = sanitizeHtml('<script src="https://www.safe.authorized.com/lib.js"></script>', {
|
|
525
|
+
allowedTags: ['script'],
|
|
526
|
+
allowedAttributes: {
|
|
527
|
+
script: ['src']
|
|
528
|
+
},
|
|
529
|
+
allowedScriptDomains: ['authorized.com'],
|
|
530
|
+
})
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
You can allow a script tag on a list of allowlisted hostnames too
|
|
534
|
+
|
|
535
|
+
```js
|
|
536
|
+
const clean = sanitizeHtml('<script src="https://www.authorized.com/lib.js"></script>', {
|
|
537
|
+
allowedTags: ['script'],
|
|
538
|
+
allowedAttributes: {
|
|
539
|
+
script: ['src']
|
|
540
|
+
},
|
|
541
|
+
allowedScriptHostnames: [ 'www.authorized.com' ],
|
|
542
|
+
})
|
|
543
|
+
```
|
|
544
|
+
|
|
503
545
|
### Allowed URL schemes
|
|
504
546
|
|
|
505
547
|
By default, we allow the following URL schemes in cases where `href`, `src`, etc. are allowed:
|
package/index.js
CHANGED
|
@@ -146,10 +146,13 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
146
146
|
allowedAttributesMap[tag].push(obj);
|
|
147
147
|
}
|
|
148
148
|
});
|
|
149
|
-
|
|
149
|
+
if (globRegex.length) {
|
|
150
|
+
allowedAttributesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
|
|
151
|
+
}
|
|
150
152
|
});
|
|
151
153
|
}
|
|
152
154
|
const allowedClassesMap = {};
|
|
155
|
+
const allowedClassesGlobMap = {};
|
|
153
156
|
each(options.allowedClasses, function(classes, tag) {
|
|
154
157
|
// Implicitly allows the class attribute
|
|
155
158
|
if (allowedAttributesMap) {
|
|
@@ -159,7 +162,18 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
159
162
|
allowedAttributesMap[tag].push('class');
|
|
160
163
|
}
|
|
161
164
|
|
|
162
|
-
allowedClassesMap[tag] =
|
|
165
|
+
allowedClassesMap[tag] = [];
|
|
166
|
+
const globRegex = [];
|
|
167
|
+
classes.forEach(function(obj) {
|
|
168
|
+
if (typeof obj === 'string' && obj.indexOf('*') >= 0) {
|
|
169
|
+
globRegex.push(escapeStringRegexp(obj).replace(/\\\*/g, '.*'));
|
|
170
|
+
} else {
|
|
171
|
+
allowedClassesMap[tag].push(obj);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
if (globRegex.length) {
|
|
175
|
+
allowedClassesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
|
|
176
|
+
}
|
|
163
177
|
});
|
|
164
178
|
|
|
165
179
|
const transformTagsMap = {};
|
|
@@ -251,6 +265,13 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
251
265
|
result = '';
|
|
252
266
|
}
|
|
253
267
|
result += '<' + name;
|
|
268
|
+
|
|
269
|
+
if (name === 'script') {
|
|
270
|
+
if (options.allowedScriptHostnames || options.allowedScriptDomains) {
|
|
271
|
+
frame.innerText = '';
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
254
275
|
if (!allowedAttributesMap || has(allowedAttributesMap, name) || allowedAttributesMap['*']) {
|
|
255
276
|
each(attribs, function(value, a) {
|
|
256
277
|
if (!VALID_HTML_ATTRIBUTE_NAME.test(a)) {
|
|
@@ -301,6 +322,33 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
301
322
|
return;
|
|
302
323
|
}
|
|
303
324
|
}
|
|
325
|
+
|
|
326
|
+
if (name === 'script' && a === 'src') {
|
|
327
|
+
|
|
328
|
+
let allowed = true;
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const parsed = new URL(value);
|
|
332
|
+
|
|
333
|
+
if (options.allowedScriptHostnames || options.allowedScriptDomains) {
|
|
334
|
+
const allowedHostname = (options.allowedScriptHostnames || []).find(function (hostname) {
|
|
335
|
+
return hostname === parsed.hostname;
|
|
336
|
+
});
|
|
337
|
+
const allowedDomain = (options.allowedScriptDomains || []).find(function(domain) {
|
|
338
|
+
return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
|
|
339
|
+
});
|
|
340
|
+
allowed = allowedHostname || allowedDomain;
|
|
341
|
+
}
|
|
342
|
+
} catch (e) {
|
|
343
|
+
allowed = false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!allowed) {
|
|
347
|
+
delete frame.attribs[a];
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
304
352
|
if (name === 'iframe' && a === 'src') {
|
|
305
353
|
let allowed = true;
|
|
306
354
|
try {
|
|
@@ -379,10 +427,17 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
379
427
|
if (a === 'class') {
|
|
380
428
|
const allowedSpecificClasses = allowedClassesMap[name];
|
|
381
429
|
const allowedWildcardClasses = allowedClassesMap['*'];
|
|
430
|
+
const allowedSpecificClassesGlob = allowedClassesGlobMap[name];
|
|
431
|
+
const allowedWildcardClassesGlob = allowedClassesGlobMap['*'];
|
|
432
|
+
const allowedClassesGlobs = [ allowedSpecificClassesGlob, allowedWildcardClassesGlob ].filter(
|
|
433
|
+
function(t) {
|
|
434
|
+
return t;
|
|
435
|
+
}
|
|
436
|
+
);
|
|
382
437
|
if (allowedSpecificClasses && allowedWildcardClasses) {
|
|
383
|
-
value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses));
|
|
438
|
+
value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses), allowedClassesGlobs);
|
|
384
439
|
} else {
|
|
385
|
-
value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses);
|
|
440
|
+
value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses, allowedClassesGlobs);
|
|
386
441
|
}
|
|
387
442
|
if (!value.length) {
|
|
388
443
|
delete frame.attribs[a];
|
|
@@ -568,7 +623,9 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
568
623
|
// a javascript: URL to be snuck through
|
|
569
624
|
href = href.replace(/<!--.*?-->/g, '');
|
|
570
625
|
// Case insensitive so we don't get faked out by JAVASCRIPT #1
|
|
571
|
-
|
|
626
|
+
// Allow more characters after the first so we don't get faked
|
|
627
|
+
// out by certain schemes browsers accept
|
|
628
|
+
const matches = href.match(/^([a-zA-Z][a-zA-Z0-9.\-+]*):/);
|
|
572
629
|
if (!matches) {
|
|
573
630
|
// Protocol-relative URL starting with any combination of '/' and '\'
|
|
574
631
|
if (href.match(/^[/\\]{2}/)) {
|
|
@@ -588,7 +645,7 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
588
645
|
}
|
|
589
646
|
|
|
590
647
|
/**
|
|
591
|
-
* Filters user input css properties by
|
|
648
|
+
* Filters user input css properties by allowlisted regex attributes.
|
|
592
649
|
*
|
|
593
650
|
* @param {object} abstractSyntaxTree - Object representation of CSS attributes.
|
|
594
651
|
* @property {array[Declaration]} abstractSyntaxTree.nodes[0] - Each object cointains prop and value key, i.e { prop: 'color', value: 'red' }.
|
|
@@ -641,10 +698,10 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
641
698
|
|
|
642
699
|
/**
|
|
643
700
|
* Filters the existing attributes for the given property. Discards any attributes
|
|
644
|
-
* which don't match the
|
|
701
|
+
* which don't match the allowlist.
|
|
645
702
|
*
|
|
646
703
|
* @param {object} selectedRule - Example: { color: red, font-family: helvetica }
|
|
647
|
-
* @param {array} allowedDeclarationsList - List of declarations which pass
|
|
704
|
+
* @param {array} allowedDeclarationsList - List of declarations which pass the allowlist.
|
|
648
705
|
* @param {object} attributeObject - Object representing the current css property.
|
|
649
706
|
* @property {string} attributeObject.type - Typically 'declaration'.
|
|
650
707
|
* @property {string} attributeObject.prop - The CSS property, i.e 'color'.
|
|
@@ -653,7 +710,7 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
653
710
|
*/
|
|
654
711
|
function filterDeclarations(selectedRule) {
|
|
655
712
|
return function (allowedDeclarationsList, attributeObject) {
|
|
656
|
-
// If this property is
|
|
713
|
+
// If this property is allowlisted...
|
|
657
714
|
if (has(selectedRule, attributeObject.prop)) {
|
|
658
715
|
const matchesRegex = selectedRule[attributeObject.prop].some(function(regularExpression) {
|
|
659
716
|
return regularExpression.test(attributeObject.value);
|
|
@@ -667,14 +724,16 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
667
724
|
};
|
|
668
725
|
}
|
|
669
726
|
|
|
670
|
-
function filterClasses(classes, allowed) {
|
|
727
|
+
function filterClasses(classes, allowed, allowedGlobs) {
|
|
671
728
|
if (!allowed) {
|
|
672
729
|
// The class attribute is allowed without filtering on this tag
|
|
673
730
|
return classes;
|
|
674
731
|
}
|
|
675
732
|
classes = classes.split(/\s+/);
|
|
676
733
|
return classes.filter(function(clss) {
|
|
677
|
-
return allowed.indexOf(clss) !== -1
|
|
734
|
+
return allowed.indexOf(clss) !== -1 || allowedGlobs.some(function(glob) {
|
|
735
|
+
return glob.test(clss);
|
|
736
|
+
});
|
|
678
737
|
}).join(' ');
|
|
679
738
|
}
|
|
680
739
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanitize-html",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Clean up user-submitted HTML, preserving
|
|
3
|
+
"version": "2.5.1",
|
|
4
|
+
"description": "Clean up user-submitted HTML, preserving allowlisted elements and allowlisted attributes on a per-element basis",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|