sanitize-html 2.3.0 → 2.4.0
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 +15 -0
- package/README.md +25 -9
- package/index.js +53 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.0 (2021-05-19):
|
|
4
|
+
- Added support for class names with wildcards in `allowedClasses`. Thanks to [zhangbenber](https://github.com/zhangbenber) for the contribution.
|
|
5
|
+
|
|
6
|
+
## 2.3.3 (2021-03-19):
|
|
7
|
+
- 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.
|
|
8
|
+
- Added a security note about the known risks associated with using the `parser` option, especially `decodeEntities: false`. See the documentation.
|
|
9
|
+
|
|
10
|
+
## 2.3.2 (2021-01-26):
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
- Updates README `yarn add` syntax. Thanks to [Tagir Khadshiev](https://github.com/Aspedm) for the contribution.
|
|
14
|
+
|
|
15
|
+
## 2.3.1 (2021-01-22):
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
3
18
|
## 2.3.0 (2020-12-16):
|
|
4
19
|
- Upgrades `htmlparser2` to new major version `^6.0.0`. Thanks to [Bogdan Chadkin](https://github.com/TrySound) for the contribution.
|
|
5
20
|
|
package/README.md
CHANGED
|
@@ -24,11 +24,21 @@ HTML comments are not preserved.
|
|
|
24
24
|
|
|
25
25
|
## Requirements
|
|
26
26
|
|
|
27
|
-
sanitize-html is intended for use with Node.
|
|
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,9 +237,10 @@ 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
|
```
|
|
@@ -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, {
|
package/index.js
CHANGED
|
@@ -5,7 +5,6 @@ const { isPlainObject } = require('is-plain-object');
|
|
|
5
5
|
const deepmerge = require('deepmerge');
|
|
6
6
|
const parseSrcset = require('parse-srcset');
|
|
7
7
|
const { parse: postcssParse } = require('postcss');
|
|
8
|
-
const url = require('url');
|
|
9
8
|
// Tags that can conceivably represent stand-alone media.
|
|
10
9
|
const mediaTags = [
|
|
11
10
|
'img', 'audio', 'video', 'picture', 'svg',
|
|
@@ -147,10 +146,13 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
147
146
|
allowedAttributesMap[tag].push(obj);
|
|
148
147
|
}
|
|
149
148
|
});
|
|
150
|
-
|
|
149
|
+
if (globRegex.length) {
|
|
150
|
+
allowedAttributesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
|
|
151
|
+
}
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
const allowedClassesMap = {};
|
|
155
|
+
const allowedClassesGlobMap = {};
|
|
154
156
|
each(options.allowedClasses, function(classes, tag) {
|
|
155
157
|
// Implicitly allows the class attribute
|
|
156
158
|
if (allowedAttributesMap) {
|
|
@@ -160,7 +162,18 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
160
162
|
allowedAttributesMap[tag].push('class');
|
|
161
163
|
}
|
|
162
164
|
|
|
163
|
-
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
|
+
}
|
|
164
177
|
});
|
|
165
178
|
|
|
166
179
|
const transformTagsMap = {};
|
|
@@ -305,12 +318,28 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
305
318
|
if (name === 'iframe' && a === 'src') {
|
|
306
319
|
let allowed = true;
|
|
307
320
|
try {
|
|
321
|
+
// Chrome accepts \ as a substitute for / in the // at the
|
|
322
|
+
// start of a URL, so rewrite accordingly to prevent exploit.
|
|
323
|
+
// Also drop any whitespace at that point in the URL
|
|
324
|
+
value = value.replace(/^(\w+:)?\s*[\\/]\s*[\\/]/, '$1//');
|
|
325
|
+
if (value.startsWith('relative:')) {
|
|
326
|
+
// An attempt to exploit our workaround for base URLs being
|
|
327
|
+
// mandatory for relative URL validation in the WHATWG
|
|
328
|
+
// URL parser, reject it
|
|
329
|
+
throw new Error('relative: exploit attempt');
|
|
330
|
+
}
|
|
308
331
|
// naughtyHref is in charge of whether protocol relative URLs
|
|
309
|
-
// are cool.
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
332
|
+
// are cool. Here we are concerned just with allowed hostnames and
|
|
333
|
+
// whether to allow relative URLs.
|
|
334
|
+
//
|
|
335
|
+
// Build a placeholder "base URL" against which any reasonable
|
|
336
|
+
// relative URL may be parsed successfully
|
|
337
|
+
let base = 'relative://relative-site';
|
|
338
|
+
for (let i = 0; (i < 100); i++) {
|
|
339
|
+
base += `/${i}`;
|
|
340
|
+
}
|
|
341
|
+
const parsed = new URL(value, base);
|
|
342
|
+
const isRelativeUrl = parsed && parsed.hostname === 'relative-site' && parsed.protocol === 'relative:';
|
|
314
343
|
if (isRelativeUrl) {
|
|
315
344
|
// default value of allowIframeRelativeUrls is true
|
|
316
345
|
// unless allowedIframeHostnames or allowedIframeDomains specified
|
|
@@ -364,10 +393,17 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
364
393
|
if (a === 'class') {
|
|
365
394
|
const allowedSpecificClasses = allowedClassesMap[name];
|
|
366
395
|
const allowedWildcardClasses = allowedClassesMap['*'];
|
|
396
|
+
const allowedSpecificClassesGlob = allowedClassesGlobMap[name];
|
|
397
|
+
const allowedWildcardClassesGlob = allowedClassesGlobMap['*'];
|
|
398
|
+
const allowedClassesGlobs = [ allowedSpecificClassesGlob, allowedWildcardClassesGlob ].filter(
|
|
399
|
+
function(t) {
|
|
400
|
+
return t;
|
|
401
|
+
}
|
|
402
|
+
);
|
|
367
403
|
if (allowedSpecificClasses && allowedWildcardClasses) {
|
|
368
|
-
value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses));
|
|
404
|
+
value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses), allowedClassesGlobs);
|
|
369
405
|
} else {
|
|
370
|
-
value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses);
|
|
406
|
+
value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses, allowedClassesGlobs);
|
|
371
407
|
}
|
|
372
408
|
if (!value.length) {
|
|
373
409
|
delete frame.attribs[a];
|
|
@@ -553,7 +589,9 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
553
589
|
// a javascript: URL to be snuck through
|
|
554
590
|
href = href.replace(/<!--.*?-->/g, '');
|
|
555
591
|
// Case insensitive so we don't get faked out by JAVASCRIPT #1
|
|
556
|
-
|
|
592
|
+
// Allow more characters after the first so we don't get faked
|
|
593
|
+
// out by certain schemes browsers accept
|
|
594
|
+
const matches = href.match(/^([a-zA-Z][a-zA-Z0-9.\-+]*):/);
|
|
557
595
|
if (!matches) {
|
|
558
596
|
// Protocol-relative URL starting with any combination of '/' and '\'
|
|
559
597
|
if (href.match(/^[/\\]{2}/)) {
|
|
@@ -652,14 +690,16 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
652
690
|
};
|
|
653
691
|
}
|
|
654
692
|
|
|
655
|
-
function filterClasses(classes, allowed) {
|
|
693
|
+
function filterClasses(classes, allowed, allowedGlobs) {
|
|
656
694
|
if (!allowed) {
|
|
657
695
|
// The class attribute is allowed without filtering on this tag
|
|
658
696
|
return classes;
|
|
659
697
|
}
|
|
660
698
|
classes = classes.split(/\s+/);
|
|
661
699
|
return classes.filter(function(clss) {
|
|
662
|
-
return allowed.indexOf(clss) !== -1
|
|
700
|
+
return allowed.indexOf(clss) !== -1 || allowedGlobs.some(function(glob) {
|
|
701
|
+
return glob.test(clss);
|
|
702
|
+
});
|
|
663
703
|
}).join(' ');
|
|
664
704
|
}
|
|
665
705
|
}
|
package/package.json
CHANGED