diff --git a/README.md b/README.md index 9471fbf..7e5bb60 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,23 @@ var html = xss(''); 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('click'); +console.log(result.html); +// Output: "<script>alert(\"xss\");</script>click" +console.log(result.removed); +// Output: [ +// { type: "tag", tag: "script", html: "", isClosing: true }, +// { type: "attr", tag: "a", attr: "onclick", value: "evil()" } +// ] +``` + ### On Browser Shim mode (reference file `test/test.html`): diff --git a/README.zh.md b/README.zh.md index f056864..0f74e33 100644 --- a/README.zh.md +++ b/README.zh.md @@ -83,6 +83,23 @@ var html = xss(''); console.log(html); ``` +### 获取过滤后的 HTML 及被移除的元素 + +可以使用 `filterXSSWithResult` 函数同时获取过滤后的 HTML 和被移除的标签/属性列表: + +```javascript +var xss = require("xss"); +var result = xss.filterXSSWithResult('click'); +console.log(result.html); +// 输出: "<script>alert(\"xss\");</script>click" +console.log(result.removed); +// 输出: [ +// { type: "tag", tag: "script", html: "", isClosing: true }, +// { type: "attr", tag: "a", attr: "onclick", value: "evil()" } +// ] +``` + ### 在浏览器端使用 Shim 模式(参考文件 `test/test.html`): diff --git a/dist/xss.js b/dist/xss.js index d189ec1..bebd6f5 100644 --- a/dist/xss.js +++ b/dist/xss.js @@ -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: "
Hello
'); -console.log(" Input :", 'Hello
'); -console.log(" Output :", r1.html); -console.log(" Removed:", JSON.stringify(r1.removed, null, 2)); -check("removed array is not empty", r1.removed.length > 0); -check("script tag was recorded", r1.removed.some(function(r) { return r.tag === "script"; })); -check("tag was NOT recorded (it is allowed)", !r1.removed.some(function(r) { return r.tag === "p"; })); - -console.log(); - -// ---- TEST 2 ---- -// onclick is NOT in the whitelist for → should be recorded as removed -console.log("TEST 2: Dangerous attribute gets recorded"); -var r2 = xss.filterXSSWithResult('click me'); -console.log(" Input :", 'click me'); -console.log(" Output :", r2.html); -console.log(" Removed:", JSON.stringify(r2.removed, null, 2)); -check("removed array has 1 item (only onclick)", r2.removed.length === 1); -check("onclick attr was recorded", r2.removed.some(function(r) { return r.attr === "onclick"; })); -check("href was NOT recorded (it is allowed)", !r2.removed.some(function(r) { return r.attr === "href"; })); - -console.log(); - -// ---- TEST 3 ---- -// Completely clean HTML → nothing should be recorded -console.log("TEST 3: Clean HTML produces empty removed array"); -var r3 = xss.filterXSSWithResult("
This is safe
"); -console.log(" Input :", "This is safe
"); -console.log(" Output :", r3.html); -console.log(" Removed:", JSON.stringify(r3.removed)); -check("removed array is empty", r3.removed.length === 0); -check("html output is unchanged", r3.html === "This is safe
"); - -console.log(); - -// ---- TEST 4 ---- -// Both a bad tag AND a bad attribute in one string -console.log("TEST 4: Mixed dangerous input — both tag and attr recorded"); -var r4 = xss.filterXSSWithResult('x'); -console.log(" Removed:", JSON.stringify(r4.removed, null, 2)); -check("script tag recorded", r4.removed.some(function(r) { return r.tag === "script" && r.type === "tag"; })); -check("onclick attr recorded", r4.removed.some(function(r) { return r.attr === "onclick" && r.type === "attr"; })); - -console.log(); - -// ---- SUMMARY ---- -console.log("========================================"); -console.log(" Results:", passed, "passed,", failed, "failed"); -if (failed === 0) { - console.log(" 🎉 ALL TESTS PASSED!"); -} else { - console.log(" ⚠️ SOME TESTS FAILED - check your code"); -} -console.log("========================================"); \ No newline at end of file diff --git a/test/test_filter_xss_with_result.js b/test/test_filter_xss_with_result.js new file mode 100644 index 0000000..f8ffef8 --- /dev/null +++ b/test/test_filter_xss_with_result.js @@ -0,0 +1,125 @@ +/** + * tests for filterXSSWithResult() function + * + * @author Joya BiswasHello
' + ); + + assert.strictEqual(result.html, '<script>alert("xss");</script>Hello
'); + assert.strictEqual(result.removed.length, 2); + assert.deepStrictEqual(result.removed[0], { + type: "tag", + tag: "script", + html: "", + isClosing: true, + }); + }); + + it("should record removed attributes", function () { + var result = xss.filterXSSWithResult( + 'click' + ); + + assert.strictEqual(result.html, 'click'); + 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("This is safe
"); + + assert.strictEqual(result.html, "This is safe
"); + assert.strictEqual(result.removed.length, 0); + }); + + it("should record both dangerous tags and attributes", function () { + var result = xss.filterXSSWithResult( + 'x' + ); + + 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('', { + 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('test', { + 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("", 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" + ); + }); +});