Implement basic XSS filter

This commit is contained in:
calzoneman 2013-10-31 00:39:35 -05:00
parent bf014530f9
commit 271a23cdad
2 changed files with 187 additions and 0 deletions

171
lib/xss.js Normal file
View File

@ -0,0 +1,171 @@
function TagParser(text) {
this.text = text;
this.i = 0;
this.tag = this.parse();
}
TagParser.prototype.skipWhitespace = function () {
while (this.i < this.text.length && this.text[this.i].match(/\s/)) {
this.i++;
}
};
TagParser.prototype.readLiteral = function (regexp) {
if (regexp === void 0) {
regexp = /[^\s>]/;
}
var str = "";
while (this.i < this.text.length && this.text[this.i].match(regexp)) {
str += this.text[this.i];
this.i++;
}
return str;
};
TagParser.prototype.readLiteralOrString = function (regexp) {
if (this.text[this.i].match(/["']/)) {
return this.readString();
}
return this.readLiteral(regexp);
};
TagParser.prototype.readString = function () {
var delim = this.text[this.i++];
var str = "";
while (this.i < this.text.length && this.text[this.i] !== delim) {
if (this.text[this.i] === "\\" && this.text[this.i+1] === delim) {
str += this.text[this.i+1];
this.i++;
} else {
str += this.text[this.i];
}
this.i++;
}
this.i++;
return str;
};
TagParser.prototype.parse = function () {
this.i = this.text.indexOf("<");
if (this.i === -1) {
return null;
}
this.i++;
this.skipWhitespace();
var tname = this.readLiteral();
// Attributes
var attrs = {};
while (this.text[this.i] !== ">") {
var key = this.readLiteralOrString(/[^\s=>]/);
this.skipWhitespace();
if (this.text[this.i] !== "=") {
if (key.trim().length > 0) {
attrs[key] = "";
}
continue;
}
this.i++;
this.skipWhitespace();
var value = this.readLiteralOrString();
if (key.trim().length > 0) {
attrs[key] = value;
}
this.skipWhitespace();
}
this.i++;
return {
tagName: tname,
attributes: attrs,
text: this.text.substring(0, this.i)
};
};
/* Some of these may not even be HTML tags, I borrowed them from the
[now deprecated] XSS module of node-validator
*/
const badTags = new RegExp([
"alert",
"applet",
"audio",
"basefont",
"base",
"behavior",
"bgsound",
"blink",
"body",
"embed",
"expression",
"form",
"frameset",
"frame",
"head",
"html",
"ilayer",
"iframe",
"input",
"layer",
"link",
"meta",
"object",
"style",
"script",
"textarea",
"title",
"video",
"xml",
"xss"
].join("|"), "i");
const badAttrs = new RegExp([
"\\bon\\S*",
"\\bformaction"
].join("|"), "i");
const badAttrValues = new RegExp([
"alert",
"document.cookie",
"expression",
"javascript",
"location",
"window"
].join("|"), "ig");
function sanitizeHTML(str) {
var i = str.indexOf("<");
if (i === -1) {
// No HTML tags
return str;
}
while (i !== -1) {
var t = new TagParser(str.substring(i)).tag;
if (t.tagName.replace("/", "").match(badTags)) {
str = str.replace(t.text, "[tag removed]");
i = str.indexOf("<", i+1);
continue;
}
for (var k in t.attributes) {
if (k.match(badAttrs)) {
delete t.attributes[k];
} else if (t.attributes[k].match(badAttrValues)) {
t.attributes[k] = t.attributes[k].replace(badAttrValues, "");
}
}
var fmt = "<" + t.tagName;
for (var k in t.attributes) {
fmt += " " + k + '="' + t.attributes[k] + '"';
}
str = str.replace(t.text, fmt + ">");
i = str.indexOf("<", i + fmt.length + 1);
}
return str;
}
module.exports.sanitizeHTML = sanitizeHTML;

16
tests/xss.js Normal file
View File

@ -0,0 +1,16 @@
var sanitize = require('../lib/xss').sanitizeHTML;
var assert = require('assert');
function basicTest() {
assert(sanitize("< script src = bad.js>blah</script>") ===
"[tag removed]blah[tag removed]");
assert(sanitize("< img src=asdf onerror='alert(\"xss\")'>") ===
"<img src=\"asdf\">");
assert(sanitize("<a href='javascript:alert(document.cookie)'>") ===
"<a href=\":()\">");
}
basicTest();
console.log("Tests passed.");