Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ var html = xss('<script>alert("xss");</script>');
console.log(html);
```

### Get sanitized HTML and removed elements

You can use `filterXSSWithResult` to get both the sanitized HTML and a list of removed tags/attributes:

```javascript
var xss = require("xss");
var result = xss.filterXSSWithResult('<script>alert("xss");</script><a href="#" onclick="evil()">click</a>');
console.log(result.html);
// Output: "&lt;script&gt;alert(\"xss\");&lt;/script&gt;<a href=\"#\">click</a>"
console.log(result.removed);
// Output: [
// { type: "tag", tag: "script", html: "<script>", isClosing: false },
// { type: "tag", tag: "script", html: "</script>", isClosing: true },
// { type: "attr", tag: "a", attr: "onclick", value: "evil()" }
// ]
```

### On Browser

Shim mode (reference file `test/test.html`):
Expand Down
17 changes: 17 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ var html = xss('<script>alert("xss");</script>');
console.log(html);
```

### 获取过滤后的 HTML 及被移除的元素

可以使用 `filterXSSWithResult` 函数同时获取过滤后的 HTML 和被移除的标签/属性列表:

```javascript
var xss = require("xss");
var result = xss.filterXSSWithResult('<script>alert("xss");</script><a href="#" onclick="evil()">click</a>');
console.log(result.html);
// 输出: "&lt;script&gt;alert(\"xss\");&lt;/script&gt;<a href=\"#\">click</a>"
console.log(result.removed);
// 输出: [
// { type: "tag", tag: "script", html: "<script>", isClosing: false },
// { type: "tag", tag: "script", html: "</script>", isClosing: true },
// { type: "attr", tag: "a", attr: "onclick", value: "evil()" }
// ]
```

### 在浏览器端使用

Shim 模式(参考文件 `test/test.html`):
Expand Down
63 changes: 63 additions & 0 deletions dist/xss.js
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,71 @@ function filterXSS(html, options) {
return xss.process(html);
}

/**
* Filter XSS and return both the sanitized HTML and a list of removed elements.
* Addresses feature request: https://github.com/leizongmin/js-xss/issues/284
*
* @param {String} html - dirty HTML string to sanitize
* @param {Object} options - same options as filterXSS()
* @return {Object} { html: String, removed: Array }
*
* Each item in `removed` is one of:
* { type: "tag", tag: "script", html: "<script>", isClosing: false }
* { type: "attr", tag: "a", attr: "onclick", value: "evil()" }
*/
function filterXSSWithResult(html, options) {
var removed = [];

// Copy options so we don't mutate the caller's object
var opts = {};
if (options) {
for (var key in options) {
opts[key] = options[key];
}
}

// Save any hooks the user may have already provided
var originalOnIgnoreTag = opts.onIgnoreTag;
var originalOnIgnoreTagAttr = opts.onIgnoreTagAttr;

// Override onIgnoreTag to record removed tags
opts.onIgnoreTag = function (tag, tagHtml, tagOptions) {
removed.push({
type: "tag",
tag: tag,
html: tagHtml,
isClosing: !!(tagOptions && tagOptions.isClosing),
});
// Still call the user's original hook if they had one
if (originalOnIgnoreTag) {
return originalOnIgnoreTag(tag, tagHtml, tagOptions);
}
};

// Override onIgnoreTagAttr to record removed attributes
opts.onIgnoreTagAttr = function (tag, name, value, isWhiteAttr) {
removed.push({
type: "attr",
tag: tag,
attr: name,
value: value,
});
// Still call the user's original hook if they had one
if (originalOnIgnoreTagAttr) {
return originalOnIgnoreTagAttr(tag, name, value, isWhiteAttr);
}
};

var cleanHtml = filterXSS(html, opts);

return {
html: cleanHtml,
removed: removed,
};
}
exports = module.exports = filterXSS;
exports.filterXSS = filterXSS;
exports.filterXSSWithResult = filterXSSWithResult;
exports.FilterXSS = FilterXSS;

(function () {
Expand Down
2 changes: 1 addition & 1 deletion dist/xss.min.js

Large diffs are not rendered by default.

85 changes: 0 additions & 85 deletions test-my-fix.js

This file was deleted.

125 changes: 125 additions & 0 deletions test/test_filter_xss_with_result.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* tests for filterXSSWithResult() function
*
* @author Joya Biswas <joyabiswas0103@gmail.com>
*/

var assert = require("assert");
var xss = require("../");

describe("filterXSSWithResult", function () {
it("should record removed tags", function () {
var result = xss.filterXSSWithResult(
'<script>alert("xss");</script><p>Hello</p>'
);

assert.strictEqual(result.html, '&lt;script&gt;alert("xss");&lt;/script&gt;<p>Hello</p>');
assert.strictEqual(result.removed.length, 2);
assert.deepStrictEqual(result.removed[0], {
type: "tag",
tag: "script",
html: "<script>",
isClosing: false,
});
assert.deepStrictEqual(result.removed[1], {
type: "tag",
tag: "script",
html: "</script>",
isClosing: true,
});
});

it("should record removed attributes", function () {
var result = xss.filterXSSWithResult(
'<a href="#" onclick="evil()">click</a>'
);

assert.strictEqual(result.html, '<a href="#">click</a>');
assert.strictEqual(result.removed.length, 1);
assert.deepStrictEqual(result.removed[0], {
type: "attr",
tag: "a",
attr: "onclick",
value: "evil()",
});
});

it("should return empty removed array for clean HTML", function () {
var result = xss.filterXSSWithResult("<p>This is safe</p>");

assert.strictEqual(result.html, "<p>This is safe</p>");
assert.strictEqual(result.removed.length, 0);
});

it("should record both dangerous tags and attributes", function () {
var result = xss.filterXSSWithResult(
'<script>bad()</script><a href="#" onclick="evil()">x</a>'
);

assert.strictEqual(result.removed.length, 3);
assert.ok(
result.removed.some(
(item) => item.type === "tag" && item.tag === "script"
),
"should have removed script tag"
);
assert.ok(
result.removed.some(
(item) => item.type === "attr" && item.attr === "onclick"
),
"should have removed onclick attribute"
);
});

it("should still call user's original onIgnoreTag hook", function () {
var called = false;
var originalTag = null;

var result = xss.filterXSSWithResult('<script>test</script>', {
onIgnoreTag: function (tag, html, options) {
called = true;
originalTag = tag;
},
});

assert.strictEqual(called, true);
assert.strictEqual(originalTag, "script");
});

it("should still call user's original onIgnoreTagAttr hook", function () {
var called = false;
var originalAttr = null;

var result = xss.filterXSSWithResult('<a onclick="evil()">test</a>', {
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
called = true;
originalAttr = name;
},
});

assert.strictEqual(called, true);
assert.strictEqual(originalAttr, "onclick");
});

it("should not mutate the original options object", function () {
var options = {
onIgnoreTag: function () {},
onIgnoreTagAttr: function () {},
};
var originalOptions = JSON.parse(JSON.stringify(options));

xss.filterXSSWithResult("<script>test</script>", options);

// Check that original hooks are preserved
assert.deepStrictEqual(
typeof options.onIgnoreTag,
"function",
"onIgnoreTag should still be a function"
);
assert.deepStrictEqual(
typeof options.onIgnoreTagAttr,
"function",
"onIgnoreTagAttr should still be a function"
);
});
});
Loading