sanitize-html
Advanced tools
Comparing version 1.1.8 to 1.2.0
82
index.js
@@ -9,3 +9,19 @@ var htmlparser = require('htmlparser2'); | ||
var result = ''; | ||
if (!options) { | ||
function Frame(tag) { | ||
var that = this; | ||
this.tag = tag; | ||
this.tagPosition = result.length; | ||
this.text = ''; // Node inner text | ||
this.updateParentNodeText = function() { | ||
if (stack.length) { | ||
var parentFrame = stack[stack.length - 1]; | ||
parentFrame.text += that.text; | ||
} | ||
}; | ||
} | ||
if (!options) { | ||
options = sanitizeHtml.defaults; | ||
@@ -37,2 +53,16 @@ } else { | ||
}); | ||
var allowedClassesMap = {}; | ||
_.each(options.allowedClasses, function(classes, tag) { | ||
// Implicitly allows the class attribute | ||
if (!allowedAttributesMap[tag]) { | ||
allowedAttributesMap[tag] = {}; | ||
} | ||
allowedAttributesMap[tag]['class'] = true; | ||
allowedClassesMap[tag] = {}; | ||
_.each(classes, function(name) { | ||
allowedClassesMap[tag][name] = true; | ||
}); | ||
}); | ||
var transformTagsMap = {}; | ||
@@ -54,8 +84,4 @@ _.each(options.transformTags, function(transform, tag){ | ||
onopentag: function(name, attribs) { | ||
stack.push({ | ||
tag: name, | ||
attribs: attribs, | ||
text: '', | ||
tagPosition: result.length | ||
}); | ||
var frame = new Frame(name); | ||
stack.push(frame); | ||
@@ -89,3 +115,2 @@ var skip = false; | ||
if (_.has(allowedAttributesMap[name], a)) { | ||
result += ' ' + a; | ||
if ((a === 'href') || (a === 'src')) { | ||
@@ -96,2 +121,9 @@ if (naughtyHref(value)) { | ||
} | ||
if (a === 'class') { | ||
value = filterClasses(value, allowedClassesMap[name]); | ||
if (!value.length) { | ||
return; | ||
} | ||
} | ||
result += ' ' + a; | ||
if (value.length) { | ||
@@ -115,9 +147,9 @@ // Values are ALREADY escaped, calling escapeHtml here | ||
} | ||
if (depth) { | ||
var frame = stack[depth - 1]; | ||
frame.text += text; | ||
} | ||
// It is NOT actually raw text, entities are already escaped. | ||
// If we call escapeHtml here we wind up double-escaping. | ||
result += text; | ||
if (stack.length) { | ||
var frame = stack[stack.length - 1]; | ||
frame.text += text; | ||
} | ||
}, | ||
@@ -130,8 +162,6 @@ onclosetag: function(name) { | ||
delete skipMap[depth]; | ||
frame.updateParentNodeText(); | ||
return; | ||
} | ||
if (_.has(selfClosingMap, name)) { | ||
// Already output /> | ||
return; | ||
} | ||
if (transformMap[depth]) { | ||
@@ -141,2 +171,3 @@ name = transformMap[depth]; | ||
} | ||
if (options.exclusiveFilter && options.exclusiveFilter(frame)) { | ||
@@ -146,2 +177,10 @@ result = result.substr(0, frame.tagPosition); | ||
} | ||
frame.updateParentNodeText(); | ||
if (_.has(selfClosingMap, name)) { | ||
// Already output /> | ||
return; | ||
} | ||
result += "</" + name + ">"; | ||
@@ -180,2 +219,13 @@ } | ||
} | ||
function filterClasses(classes, allowed) { | ||
if (!allowed) { | ||
// The class attribute is allowed without filtering on this tag | ||
return classes; | ||
} | ||
classes = classes.split(/\s+/); | ||
return _.filter(classes, function(c) { | ||
return _.has(allowed, c); | ||
}).join(' '); | ||
} | ||
} | ||
@@ -182,0 +232,0 @@ |
{ | ||
"name": "sanitize-html", | ||
"version": "1.1.8", | ||
"version": "1.2.0", | ||
"description": "Clean up user-submitted HTML, preserving whitelisted elements and whitelisted attributes on a per-element basis", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -130,2 +130,18 @@ # sanitize-html | ||
### Allowed CSS Classes | ||
If you wish to allow specific CSS classes on a particular element, you can do so with the `allowedClasses` option. Any other CSS classes are discarded. | ||
This implies that the `class` attribute is allowed on that element. | ||
```javascript | ||
// Allow only a restricted set of CSS classes and only on the p tag | ||
clean = sanitizeHtml(dirty, { | ||
allowedTags: [ 'p', 'em', 'strong' ], | ||
allowedClasses: { | ||
'p': [ 'fancy', 'simple' ] | ||
} | ||
}); | ||
``` | ||
### Allowed URL schemes | ||
@@ -152,2 +168,10 @@ | ||
1.2.0: | ||
* The `allowedClasses` option now allows you to permit CSS classes in a fine-grained way. | ||
* Text passed to your `exclusiveFilter` function now includes the text of child elements, making it more useful for identifying elements that truly lack any inner text. | ||
1.1.7: use `he` for entity decoding, because it is more actively maintained. | ||
1.1.6: `allowedSchemes` option for those who want to permit `data` URLs and such. | ||
@@ -154,0 +178,0 @@ |
105
test/test.js
@@ -29,6 +29,6 @@ var assert = require("assert"); | ||
it('should reject hrefs that are not relative, ftp, http, https or mailto', function() { | ||
assert.equal(sanitizeHtml('<a href="http://google.com">google</a><a href="https://google.com">https google</a><a href="ftp://example.com">ftp</a><a href="mailto:test@test.com">mailto</a><a href="/relative.html">relative</a><a href="javascript:alert(0)">javascript</a>'), '<a href="http://google.com">google</a><a href="https://google.com">https google</a><a href="ftp://example.com">ftp</a><a href="mailto:test@test.com">mailto</a><a href="/relative.html">relative</a><a href>javascript</a>'); | ||
assert.equal(sanitizeHtml('<a href="http://google.com">google</a><a href="https://google.com">https google</a><a href="ftp://example.com">ftp</a><a href="mailto:test@test.com">mailto</a><a href="/relative.html">relative</a><a href="javascript:alert(0)">javascript</a>'), '<a href="http://google.com">google</a><a href="https://google.com">https google</a><a href="ftp://example.com">ftp</a><a href="mailto:test@test.com">mailto</a><a href="/relative.html">relative</a><a>javascript</a>'); | ||
}); | ||
it('should cope identically with capitalized attributes and tags and should tolerate capitalized schemes', function() { | ||
assert.equal(sanitizeHtml('<A HREF="http://google.com">google</a><a href="HTTPS://google.com">https google</a><a href="ftp://example.com">ftp</a><a href="mailto:test@test.com">mailto</a><a href="/relative.html">relative</a><a href="javascript:alert(0)">javascript</a>'), '<a href="http://google.com">google</a><a href="HTTPS://google.com">https google</a><a href="ftp://example.com">ftp</a><a href="mailto:test@test.com">mailto</a><a href="/relative.html">relative</a><a href>javascript</a>'); | ||
assert.equal(sanitizeHtml('<A HREF="http://google.com">google</a><a href="HTTPS://google.com">https google</a><a href="ftp://example.com">ftp</a><a href="mailto:test@test.com">mailto</a><a href="/relative.html">relative</a><a href="javascript:alert(0)">javascript</a>'), '<a href="http://google.com">google</a><a href="HTTPS://google.com">https google</a><a href="ftp://example.com">ftp</a><a href="mailto:test@test.com">mailto</a><a href="/relative.html">relative</a><a>javascript</a>'); | ||
}); | ||
@@ -48,12 +48,12 @@ it('should drop the content of script elements', function() { | ||
it('should dump a sneaky encoded javascript url', function() { | ||
assert.equal(sanitizeHtml('<a href="javascript:alert('XSS')">Hax</a>'), '<a href>Hax</a>'); | ||
assert.equal(sanitizeHtml('<a href="javascript:alert('XSS')">Hax</a>'), '<a>Hax</a>'); | ||
}); | ||
it('should dump an uppercase javascript url', function() { | ||
assert.equal(sanitizeHtml('<a href="JAVASCRIPT:alert(\'foo\')">Hax</a>'), '<a href>Hax</a>'); | ||
assert.equal(sanitizeHtml('<a href="JAVASCRIPT:alert(\'foo\')">Hax</a>'), '<a>Hax</a>'); | ||
}); | ||
it('should dump character codes 1-32 before testing scheme', function() { | ||
assert.equal(sanitizeHtml('<a href="java\0\t\r\n script:alert(\'foo\')">Hax</a>'), '<a href>Hax</a>'); | ||
assert.equal(sanitizeHtml('<a href="java\0\t\r\n script:alert(\'foo\')">Hax</a>'), '<a>Hax</a>'); | ||
}); | ||
it('should dump character codes 1-32 even when escaped with padding rather than trailing ;', function() { | ||
assert.equal(sanitizeHtml('<a href="java�script:alert(\'foo\')">Hax</a>'), '<a href>Hax</a>'); | ||
assert.equal(sanitizeHtml('<a href="java�script:alert(\'foo\')">Hax</a>'), '<a>Hax</a>'); | ||
}); | ||
@@ -88,13 +88,56 @@ it('should still like nice schemes', function() { | ||
}); | ||
it('should skip empty a', function() { | ||
assert.equal( | ||
sanitizeHtml('<p>This is <a href="http://www.linux.org"></a><br/>Linux</p>', | ||
{ | ||
exclusiveFilter : function(frame) { | ||
return frame.tag === 'a' && !frame.text.trim(); | ||
} | ||
}), | ||
'<p>This is <br />Linux</p>' | ||
); | ||
it('should skip an empty link', function() { | ||
assert.strictEqual( | ||
sanitizeHtml('<p>This is <a href="http://www.linux.org"></a><br/>Linux</p>', { | ||
exclusiveFilter: function (frame) { | ||
return frame.tag === 'a' && !frame.text.trim(); | ||
} | ||
}), | ||
'<p>This is <br />Linux</p>' | ||
); | ||
}); | ||
it("Should expose a node's inner text and inner HTML to the filter", function() { | ||
assert.strictEqual( | ||
sanitizeHtml('<p>12<a href="http://www.linux.org"><br/>3<br></a><span>4</span></p>', { | ||
exclusiveFilter : function(frame) { | ||
if (frame.tag === 'p') { | ||
assert.strictEqual(frame.text, '124'); | ||
} else if (frame.tag === 'a') { | ||
assert.strictEqual(frame.text, '3'); | ||
return true; | ||
} else if (frame.tag === 'br') { | ||
assert.strictEqual(frame.text, ''); | ||
} else { | ||
assert.fail('p, a, br', frame.tag); | ||
} | ||
return false; | ||
} | ||
}), | ||
'<p>124</p>' | ||
); | ||
}); | ||
it('Should collapse nested empty elements', function() { | ||
assert.strictEqual( | ||
sanitizeHtml('<p><a href="http://www.linux.org"><br/></a></p>', { | ||
exclusiveFilter : function(frame) { | ||
return (frame.tag === 'a' || frame.tag === 'p' ) && !frame.text.trim(); | ||
} | ||
}), | ||
'' | ||
); | ||
}); | ||
it('Exclusive filter should not affect elements which do not match the filter condition', function () { | ||
assert.strictEqual( | ||
sanitizeHtml('I love <a href="www.linux.org" target="_hplink">Linux</a> OS', | ||
{ | ||
exclusiveFilter: function (frame) { | ||
return (frame.tag === 'a') && !frame.text.trim(); | ||
} | ||
}), | ||
'I love <a href="www.linux.org" target="_hplink">Linux</a> OS' | ||
); | ||
}); | ||
it('should disallow data URLs with default allowedSchemes', function() { | ||
@@ -109,3 +152,3 @@ assert.equal( | ||
), | ||
'<img src />' | ||
'<img />' | ||
); | ||
@@ -126,3 +169,31 @@ }); | ||
}); | ||
it('should allow specific classes when whitelisted with allowedClasses', function() { | ||
assert.equal( | ||
sanitizeHtml( | ||
'<p class="nifty simple dippy">whee</p>', | ||
{ | ||
allowedTags: [ 'p' ], | ||
allowedClasses: { | ||
p: [ 'nifty' ] | ||
} | ||
} | ||
), | ||
'<p class="nifty">whee</p>' | ||
); | ||
}); | ||
it('should not act weird when the class attribute is empty', function() { | ||
assert.equal( | ||
sanitizeHtml( | ||
'<p class="">whee</p>', | ||
{ | ||
allowedTags: [ 'p' ], | ||
allowedClasses: { | ||
p: [ 'nifty' ] | ||
} | ||
} | ||
), | ||
'<p>whee</p>' | ||
); | ||
}); | ||
}); | ||
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
31552
427
213