跨站脚本攻击
This commit is contained in:
liwu0213
2017-03-15 12:45:03 +08:00
committed by GitHub
parent b408af99dc
commit 90d8258b4e
35 changed files with 6379 additions and 2 deletions

2
.coveralls.yml Normal file
View File

@@ -0,0 +1,2 @@
service_name: travis-pro
repo_token: 9WQeMOiEjFQQAG2FdKJYZdKuYKLszsjEA

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
node_modules
npm-debug.log
benchmark/result*.html
coverage.html

7
.travis.yml Normal file
View File

@@ -0,0 +1,7 @@
language: node_js
node_js:
- 0.10
- 0.12
- 4.0
- 5.0
- 6.0

1
AUTHORS Normal file
View File

@@ -0,0 +1 @@
Zongmin Lei <leizongmin@gmail.com> (http://ucdok.com)

23
LICENSE.md Normal file
View File

@@ -0,0 +1,23 @@
Copyright (c) 2012-2016 Zongmin Lei(雷宗民) <leizongmin@gmail.com>
http://ucdok.com
The MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

507
README.md
View File

@@ -1,2 +1,505 @@
# js-xss
js解决跨站脚本攻击
[![NPM version][npm-image]][npm-url]
[![build status][travis-image]][travis-url]
[![Test coverage][coveralls-image]][coveralls-url]
[![David deps][david-image]][david-url]
[![node version][node-image]][node-url]
[![npm download][download-image]][download-url]
[![npm license][license-image]][download-url]
[npm-image]: https://img.shields.io/npm/v/xss.svg?style=flat-square
[npm-url]: https://npmjs.org/package/xss
[travis-image]: https://img.shields.io/travis/leizongmin/js-xss.svg?style=flat-square
[travis-url]: https://travis-ci.org/leizongmin/js-xss
[coveralls-image]: https://img.shields.io/coveralls/leizongmin/js-xss.svg?style=flat-square
[coveralls-url]: https://coveralls.io/r/leizongmin/js-xss?branch=master
[david-image]: https://img.shields.io/david/leizongmin/js-xss.svg?style=flat-square
[david-url]: https://david-dm.org/leizongmin/js-xss
[node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square
[node-url]: http://nodejs.org/download/
[download-image]: https://img.shields.io/npm/dm/xss.svg?style=flat-square
[download-url]: https://npmjs.org/package/xss
[license-image]: https://img.shields.io/npm/l/xss.svg
Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist.
======
![xss](https://nodei.co/npm/xss.png?downloads=true&stars=true)
--------------
`xss` is a module used to filter input from users to prevent XSS attacks.
([What is XSS attack?](http://en.wikipedia.org/wiki/Cross-site_scripting))
**Project Homepage:** http://jsxss.com
**Try Online:** http://jsxss.com/en/try.html
**[中文版文档](https://github.com/leizongmin/js-xss/blob/master/README.zh.md)**
---------------
## Features
+ Specifies HTML tags and their attributes allowed with whitelist
+ Handle any tags or attributes using custom function.
## Reference
+ [XSS Filter Evasion Cheat Sheet](https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet)
+ [Data URI scheme](http://en.wikipedia.org/wiki/Data_URI_scheme)
+ [XSS with Data URI Scheme](http://hi.baidu.com/badzzzz/item/bdbafe83144619c199255f7b)
## Benchmark (for references only)
+ the xss module: 8.2 MB/s
+ `xss()` function from module `validator@0.3.7`: 4.4 MB/s
For test code please refer to `benchmark` directory.
## They are using xss module
+ **nodeclub** - A Node.js bbs using MongoDB - https://github.com/cnodejs/nodeclub
+ **cnpmjs.org** - Private npm registry and web for Enterprise - https://github.com/cnpm/cnpmjs.org
## Install
### NPM
```bash
$ npm install xss
```
### Bower
```bash
$ bower install xss
```
Or
```bash
$ bower install https://github.com/leizongmin/js-xss.git
```
## Usages
### On Node.js
```javascript
var xss = require('xss');
var html = xss('<script>alert("xss");</script>');
console.log(html);
```
### On Browser
Shim mode (reference file `test/test.html`):
```html
<script src="https://raw.github.com/leizongmin/js-xss/master/dist/xss.js"></script>
<script>
// apply function filterXSS in the same way
var html = filterXSS('<script>alert("xss");</scr' + 'ipt>');
alert(html);
</script>
```
AMD mode - shim:
```html
<script>
require.config({
baseUrl: './',
paths: {
xss: 'https://raw.github.com/leizongmin/js-xss/master/dist/xss.js'
},
shim: {
xss: {exports: 'filterXSS'}
}
})
require(['xss'], function (xss) {
var html = xss('<script>alert("xss");</scr' + 'ipt>');
alert(html);
});
</script>
```
## Command Line Tool
### Process File
You can use the xss command line tool to process a file. Usage:
```bash
xss -i <input_file> -o <output_file>
```
Example:
```bash
$ xss -i origin.html -o target.html
```
### Active Test
Run the following command, them you can type HTML
code in the command-line, and check the filtered output:
```bash
$ xss -t
```
For more details, please run `$ xss -h` to see it.
## Custom filter rules
When using the `xss()` function, the second parameter could be used to specify
custom rules:
```javascript
options = {}; // Custom rules
html = xss('<script>alert("xss");</script>', options);
```
To avoid passing `options` every time, you can also do it in a faster way by
creating a `FilterXSS` instance:
```javascript
options = {}; // Custom rules
myxss = new xss.FilterXSS(options);
// then apply myxss.process()
html = myxss.process('<script>alert("xss");</script>');
```
Details of parameters in `options` would be described below.
### Whitelist
By specifying a `whiteList`, e.g. `{ 'tagName': [ 'attr-1', 'attr-2' ] }`. Tags
and attributes not in the whitelist would be filter out. For example:
```javascript
// only tag a and its attributes href, title, target are allowed
var options = {
whiteList: {
a: ['href', 'title', 'target']
}
};
// With the configuration specified above, the following HTML:
// <a href="#" onclick="hello()"><i>Hello</i></a>
// would become:
// <a href="#">Hello</a>
```
For the default whitelist, please refer `xss.whiteList`.
### Customize the handler function for matched tags
By specifying the handler function with `onTag`:
```javascript
function onTag (tag, html, options) {
// tag is the name of current tag, e.g. 'a' for tag <a>
// html is the HTML of this tag, e.g. '<a>' for tag <a>
// options is some addition informations:
// isWhite boolean, whether the tag is in whitelist
// isClosing boolean, whether the tag is a closing tag, e.g. true for </a>
// position integer, the position of the tag in output result
// sourcePosition integer, the position of the tag in input HTML source
// If a string is returned, the current tag would be replaced with the string
// If return nothing, the default measure would be taken:
// If in whitelist: filter attributes using onTagAttr, as described below
// If not in whitelist: handle by onIgnoreTag, as described below
}
```
### Customize the handler function for attributes of matched tags
By specifying the handler function with `onTagAttr`:
```javascript
function onTagAttr (tag, name, value, isWhiteAttr) {
// tag is the name of current tag, e.g. 'a' for tag <a>
// name is the name of current attribute, e.g. 'href' for href="#"
// isWhiteAttr whether the attribute is in whitelist
// If a string is returned, the attribute would be replaced with the string
// If return nothing, the default measure would be taken:
// If in whitelist: filter the value using safeAttrValue as described below
// If not in whitelist: handle by onIgnoreTagAttr, as described below
}
```
### Customize the handler function for tags not in the whitelist
By specifying the handler function with `onIgnoreTag`:
```javascript
function onIgnoreTag (tag, html, options) {
// Parameters are the same with onTag
// If a string is returned, the tag would be replaced with the string
// If return nothing, the default measure would be taken (specifies using
// escape, as described below)
}
```
### Customize the handler function for attributes not in the whitelist
By specifying the handler function with `onIgnoreTagAttr`:
```javascript
function onIgnoreTagAttr (tag, name, value, isWhiteAttr) {
// Parameters are the same with onTagAttr
// If a string is returned, the value would be replaced with this string
// If return nothing, then keep default (remove the attribute)
}
```
### Customize escaping function for HTML
By specifying the handler function with `escapeHtml`. Following is the default
function **(Modification is not recommended)**:
```javascript
function escapeHtml (html) {
return html.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
```
### Customize escaping function for value of attributes
By specifying the handler function with `safeAttrValue`:
```javascript
function safeAttrValue (tag, name, value) {
// Parameters are the same with onTagAttr (without options)
// Return the value as a string
}
```
### Customize CSS filter
If you allow the attribute `style`, the value will be processed by [cssfilter](https://github.com/leizongmin/js-css-filter) module. The cssfilter module includes a default css whitelist. You can specify the options for cssfilter module like this:
```javascript
myxss = new xss.FilterXSS({
css: {
whiteList: {
position: /^fixed|relative$/,
top: true,
left: true,
}
}
});
html = myxss.process('<script>alert("xss");</script>');
```
If you don't want to filter out the `style` content, just specify `false` to the `css` option:
```javascript
myxss = new xss.FilterXSS({
css: false,
});
```
For more help, please see https://github.com/leizongmin/js-css-filter
### Quick Start
#### Filter out tags not in the whitelist
By using `stripIgnoreTag` parameter:
+ `true` filter out tags not in the whitelist
+ `false`: by default: escape the tag using configured `escape` function
Example:
If `stripIgnoreTag = true` is set, the following code:
```html
code:<script>alert(/xss/);</script>
```
would output filtered:
```html
code:alert(/xss/);
```
#### Filter out tags and tag bodies not in the whitelist
By using `stripIgnoreTagBody` parameter:
+ `false|null|undefined` by default: do nothing
+ `'*'|true`: filter out all tags not in the whitelist
+ `['tag1', 'tag2']`: filter out only specified tags not in the whitelist
Example:
If `stripIgnoreTagBody = ['script']` is set, the following code:
```html
code:<script>alert(/xss/);</script>
```
would output filtered:
```html
code:
```
#### Filter out HTML comments
By using `allowCommentTag` parameter:
+ `true`: do nothing
+ `false` by default: filter out HTML comments
Example:
If `allowCommentTag = false` is set, the following code:
```html
code:<!-- something --> END
```
would output filtered:
```html
code: END
```
## Examples
### Allow attributes of whitelist tags start with `data-`
```javascript
var source = '<div a="1" b="2" data-a="3" data-b="4">hello</div>';
var html = xss(source, {
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
if (name.substr(0, 5) === 'data-') {
// escape its value using built-in escapeAttrValue function
return name + '="' + xss.escapeAttrValue(value) + '"';
}
}
});
console.log('%s\nconvert to:\n%s', source, html);
```
Result:
```html
<div a="1" b="2" data-a="3" data-b="4">hello</div>
convert to:
<div data-a="3" data-b="4">hello</div>
```
### Allow tags start with `x-`
```javascript
var source = '<x><x-1>he<x-2 checked></x-2>wwww</x-1><a>';
var html = xss(source, {
onIgnoreTag: function (tag, html, options) {
if (tag.substr(0, 2) === 'x-') {
// do not filter its attributes
return html;
}
}
});
console.log('%s\nconvert to:\n%s', source, html);
```
Result:
```html
<x><x-1>he<x-2 checked></x-2>wwww</x-1><a>
convert to:
&lt;x&gt;<x-1>he<x-2 checked></x-2>wwww</x-1><a>
```
### Parse images in HTML
```javascript
var source = '<img src="img1">a<img src="img2">b<img src="img3">c<img src="img4">d';
var list = [];
var html = xss(source, {
onTagAttr: function (tag, name, value, isWhiteAttr) {
if (tag === 'img' && name === 'src') {
// Use the built-in friendlyAttrValue function to escape attribute
// values. It supports converting entity tags such as &lt; to printable
// characters such as <
list.push(xss.friendlyAttrValue(value));
}
// Return nothing, means keep the default handling measure
}
});
console.log('image list:\n%s', list.join(', '));
```
Result:
```html
image list:
img1, img2, img3, img4
```
### Filter out HTML tags (keeps only plain text)
```javascript
var source = '<strong>hello</strong><script>alert(/xss/);</script>end';
var html = xss(source, {
whiteList: [], // empty, means filter out all tags
stripIgnoreTag: true, // filter out all HTML not in the whilelist
stripIgnoreTagBody: ['script'] // the script tag is a special case, we need
// to filter out its content
});
console.log('text: %s', html);
```
Result:
```html
text: helloend
```
## License
```
Copyright (c) 2012-2016 Zongmin Lei(雷宗民) <leizongmin@gmail.com>
http://ucdok.com
The MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

497
README.zh.md Normal file
View File

@@ -0,0 +1,497 @@
[![NPM version][npm-image]][npm-url]
[![build status][travis-image]][travis-url]
[![Test coverage][coveralls-image]][coveralls-url]
[![David deps][david-image]][david-url]
[![node version][node-image]][node-url]
[![npm download][download-image]][download-url]
[![npm license][license-image]][download-url]
[npm-image]: https://img.shields.io/npm/v/xss.svg?style=flat-square
[npm-url]: https://npmjs.org/package/xss
[travis-image]: https://img.shields.io/travis/leizongmin/js-xss.svg?style=flat-square
[travis-url]: https://travis-ci.org/leizongmin/js-xss
[coveralls-image]: https://img.shields.io/coveralls/leizongmin/js-xss.svg?style=flat-square
[coveralls-url]: https://coveralls.io/r/leizongmin/js-xss?branch=master
[david-image]: https://img.shields.io/david/leizongmin/js-xss.svg?style=flat-square
[david-url]: https://david-dm.org/leizongmin/js-xss
[node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square
[node-url]: http://nodejs.org/download/
[download-image]: https://img.shields.io/npm/dm/xss.svg?style=flat-square
[download-url]: https://npmjs.org/package/xss
[license-image]: https://img.shields.io/npm/l/xss.svg
根据白名单过滤HTML(防止XSS攻击)
======
![xss](https://nodei.co/npm/xss.png?downloads=true&stars=true)
--------------
`xss`是一个用于对用户输入的内容进行过滤以避免遭受XSS攻击的模块
[什么是XSS攻击](http://baike.baidu.com/view/2161269.htm))。主要用于论坛、博客、网上商店等等一些可允许用户录入页面排版、
格式控制相关的HTML的场景`xss`模块通过白名单来控制允许的标签及相关的标签属性,
另外还提供了一系列的接口以便用户扩展,比其他同类模块更为灵活。
**项目主页:** http://jsxss.com
**在线测试:** http://jsxss.com/zh/try.html
---------------
## 特性
+ 白名单控制允许的HTML标签及各标签的属性
+ 通过自定义处理函数,可对任意标签及其属性进行处理
## 参考资料
+ [XSS与字符编码的那些事儿 ---科普文](http://drops.wooyun.org/tips/689)
+ [腾讯实例教程那些年我们一起学XSS](http://www.wooyun.org/whitehats/%E5%BF%83%E4%BC%A4%E7%9A%84%E7%98%A6%E5%AD%90)
+ [mXSS攻击的成因及常见种类](http://drops.wooyun.org/tips/956)
+ [XSS Filter Evasion Cheat Sheet](https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet)
+ [Data URI scheme](http://en.wikipedia.org/wiki/Data_URI_scheme)
+ [XSS with Data URI Scheme](http://hi.baidu.com/badzzzz/item/bdbafe83144619c199255f7b)
## 性能(仅作参考)
+ xss模块8.2 MB/s
+ validator@0.3.7模块的xss()函数4.4 MB/s
测试代码参考 benchmark 目录
## 安装
### NPM
```bash
$ npm install xss
```
### Bower
```bash
$ bower install xss
```
或者
```bash
$ bower install https://github.com/leizongmin/js-xss.git
```
## 使用方法
### 在Node.js中使用
```javascript
var xss = require('xss');
var html = xss('<script>alert("xss");</script>');
console.log(html);
```
### 在浏览器端使用
Shim模式参考文件 `test/test.html`:
```html
<script src="https://raw.github.com/leizongmin/js-xss/master/dist/xss.js"></script>
<script>
// 使用函数名 filterXSS用法一样
var html = filterXSS('<script>alert("xss");</scr' + 'ipt>');
alert(html);
</script>
```
AMD模式参考文件 `test/test_amd.html`:
```html
<script>
require.config({
baseUrl: './',
paths: {
xss: 'https://raw.github.com/leizongmin/js-xss/master/dist/xss.js'
},
shim: {
xss: {exports: 'filterXSS'}
}
})
require(['xss'], function (xss) {
var html = xss('<script>alert("xss");</scr' + 'ipt>');
alert(html);
});
</script>
```
### 使用命令行工具来对文件进行XSS处理
### 处理文件
可通过内置的 `xss` 命令来对输入的文件进行XSS处理。使用方法
```bash
xss -i <源文件> -o <目标文件>
```
例:
```bash
$ xss -i origin.html -o target.html
```
### 在线测试
执行以下命令可在命令行中输入HTML代码并看到过滤后的代码
```bash
$ xss -t
```
详细命令行参数说明,请输入 `$ xss -h` 来查看。
## 自定义过滤规则
在调用 `xss()` 函数进行过滤时,可通过第二个参数来设置自定义规则:
```javascript
options = {}; // 自定义规则
html = xss('<script>alert("xss");</script>', options);
```
如果不想每次都传入一个 `options` 参数,可以创建一个 `FilterXSS` 实例
(使用这种方法速度更快):
```
options = {}; // 自定义规则
myxss = new xss.FilterXSS(options);
// 以后直接调用 myxss.process() 来处理即可
html = myxss.process('<script>alert("xss");</script>');
```
`options` 参数的详细说明见下文。
### 白名单
通过 `whiteList` 来指定,格式为:`{'标签名': ['属性1', '属性2']}`。不在白名单上
的标签将被过滤,不在白名单上的属性也会被过滤。以下是示例:
```javascript
// 只允许a标签该标签只允许href, title, target这三个属性
var options = {
whiteList: {
a: ['href', 'title', 'target']
}
};
// 使用以上配置后下面的HTML
// <a href="#" onclick="hello()"><i>大家好</i></a>
// 将被过滤为
// <a href="#">大家好</a>
```
默认白名单参考 `xss.whiteList`
### 自定义匹配到标签时的处理方法
通过 `onTag` 来指定相应的处理函数。以下是详细说明:
```javascript
function onTag (tag, html, options) {
// tag是当前的标签名称比如<a>标签则tag的值是'a'
// html是该标签的HTML比如<a>标签则html的值是'<a>'
// options是一些附加的信息具体如下
// isWhite boolean类型表示该标签是否在白名单上
// isClosing boolean类型表示该标签是否为闭合标签比如</a>时为true
// position integer类型表示当前标签在输出的结果中的起始位置
// sourcePosition integer类型表示当前标签在原HTML中的起始位置
// 如果返回一个字符串,则当前标签将被替换为该字符串
// 如果不返回任何值,则使用默认的处理方法:
// 在白名单上: 通过onTagAttr来过滤属性详见下文
// 不在白名单上通过onIgnoreTag指定详见下文
}
```
### 自定义匹配到标签的属性时的处理方法
通过 `onTagAttr` 来指定相应的处理函数。以下是详细说明:
```javascript
function onTagAttr (tag, name, value, isWhiteAttr) {
// tag是当前的标签名称比如<a>标签则tag的值是'a'
// name是当前属性的名称比如href="#"则name的值是'href'
// value是当前属性的值比如href="#"则value的值是'#'
// isWhiteAttr是否为白名单上的属性
// 如果返回一个字符串,则当前属性值将被替换为该字符串
// 如果不返回任何值,则使用默认的处理方法
// 在白名单上: 调用safeAttrValue来过滤属性值并输出该属性详见下文
// 不在白名单上通过onIgnoreTagAttr指定详见下文
}
```
### 自定义匹配到不在白名单上的标签时的处理方法
通过 `onIgnoreTag` 来指定相应的处理函数。以下是详细说明:
```javascript
function onIgnoreTag (tag, html, options) {
// 参数说明与onTag相同
// 如果返回一个字符串,则当前标签将被替换为该字符串
// 如果不返回任何值则使用默认的处理方法通过escape指定详见下文
}
```
### 自定义匹配到不在白名单上的属性时的处理方法
通过 `onIgnoreTagAttr` 来指定相应的处理函数。以下是详细说明:
```javascript
function onIgnoreTagAttr (tag, name, value, isWhiteAttr) {
// 参数说明与onTagAttr相同
// 如果返回一个字符串,则当前属性值将被替换为该字符串
// 如果不返回任何值,则使用默认的处理方法(删除该属)
}
```
### 自定义HTML转义函数
通过 `escapeHtml` 来指定相应的处理函数。以下是默认代码 **(不建议修改)**
```javascript
function escapeHtml (html) {
return html.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
```
### 自定义标签属性值的转义函数
通过 `safeAttrValue` 来指定相应的处理函数。以下是详细说明:
```javascript
function safeAttrValue (tag, name, value) {
// 参数说明与onTagAttr相同没有options参数
// 返回一个字符串表示该属性值
}
```
### 自定义CSS过滤器
如果配置中允许了标签的 `style` 属性,则它的值会通过[cssfilter](https://github.com/leizongmin/js-css-filter) 模块处理。
`cssfilter` 模块包含了一个默认的CSS白名单你可以通过以下的方式配置
```javascript
myxss = new xss.FilterXSS({
css: {
whiteList: {
position: /^fixed|relative$/,
top: true,
left: true,
}
}
});
html = myxss.process('<script>alert("xss");</script>');
```
如果不想使用 CSS 过滤器来处理 `style` 属性的内容,可指定 `css` 选项的值为 `false`
```javascript
myxss = new xss.FilterXSS({
css: false,
});
```
要获取更多的帮助信息可看这里https://github.com/leizongmin/js-css-filter
### 快捷配置
#### 去掉不在白名单上的标签
通过 `stripIgnoreTag` 来设置:
+ `true`:去掉不在白名单上的标签
+ `false`:(默认),使用配置的`escape`函数对该标签进行转义
示例:
当设置 `stripIgnoreTag = true`时,以下代码
```html
code:<script>alert(/xss/);</script>
```
过滤后将输出
```html
code:alert(/xss/);
```
#### 去掉不在白名单上的标签及标签体
通过 `stripIgnoreTagBody` 来设置:
+ `false|null|undefined`:(默认),不特殊处理
+ `'*'|true`:去掉所有不在白名单上的标签
+ `['tag1', 'tag2']`:仅去掉指定的不在白名单上的标签
示例:
当设置 `stripIgnoreTagBody = ['script']`时,以下代码
```html
code:<script>alert(/xss/);</script>
```
过滤后将输出
```html
code:
```
#### 去掉HTML备注
通过 `allowCommentTag` 来设置:
+ `true`:不处理
+ `false`默认自动去掉HTML中的备注
示例:
当设置 `allowCommentTag = false` 时,以下代码
```html
code:<!-- something --> END
```
过滤后将输出
```html
code: END
```
## 应用实例
### 允许标签以data-开头的属性
```javascript
var source = '<div a="1" b="2" data-a="3" data-b="4">hello</div>';
var html = xss(source, {
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
if (name.substr(0, 5) === 'data-') {
// 通过内置的escapeAttrValue函数来对属性值进行转义
return name + '="' + xss.escapeAttrValue(value) + '"';
}
}
});
console.log('%s\nconvert to:\n%s', source, html);
```
运行结果:
```html
<div a="1" b="2" data-a="3" data-b="4">hello</div>
convert to:
<div data-a="3" data-b="4">hello</div>
```
### 允许名称以x-开头的标签
```javascript
var source = '<x><x-1>he<x-2 checked></x-2>wwww</x-1><a>';
var html = xss(source, {
onIgnoreTag: function (tag, html, options) {
if (tag.substr(0, 2) === 'x-') {
// 不对其属性列表进行过滤
return html;
}
}
});
console.log('%s\nconvert to:\n%s', source, html);
```
运行结果:
```html
<x><x-1>he<x-2 checked></x-2>wwww</x-1><a>
convert to:
&lt;x&gt;<x-1>he<x-2 checked></x-2>wwww</x-1><a>
```
### 分析HTML代码中的图片列表
```javascript
var source = '<img src="img1">a<img src="img2">b<img src="img3">c<img src="img4">d';
var list = [];
var html = xss(source, {
onTagAttr: function (tag, name, value, isWhiteAttr) {
if (tag === 'img' && name === 'src') {
// 使用内置的friendlyAttrValue函数来对属性值进行转义可将&lt;这类的实体标记转换成打印字符<
list.push(xss.friendlyAttrValue(value));
}
// 不返回任何值,表示还是按照默认的方法处理
}
});
console.log('image list:\n%s', list.join(', '));
```
运行结果:
```html
image list:
img1, img2, img3, img4
```
### 去除HTML标签只保留文本内容
```javascript
var source = '<strong>hello</strong><script>alert(/xss/);</script>end';
var html = xss(source, {
whiteList: [], // 白名单为空,表示过滤所有标签
stripIgnoreTag: true, // 过滤所有非白名单标签的HTML
stripIgnoreTagBody: ['script'] // script标签较特殊需要过滤标签中间的内容
});
console.log('text: %s', html);
```
运行结果:
```html
text: helloend
```
## 授权协议
```
Copyright (c) 2012-2016 Zongmin Lei(雷宗民) <leizongmin@gmail.com>
http://ucdok.com
The MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

1471
benchmark/file.html Normal file

File diff suppressed because it is too large Load Diff

50
benchmark/index.js Normal file
View File

@@ -0,0 +1,50 @@
/**
* 性能测试
*/
var xss = require('../');
var fs = require('fs');
// 读取样例
var html = fs.readFileSync(__dirname + '/file.html', 'utf8');
var COUNT = 200;
var ret = '';
var timeStart = Date.now();
for (var i = 0; i < COUNT; i++) {
ret = xss(html);
}
var timeEnd = Date.now();
var spent = timeEnd - timeStart;
var speed = (((html.length * i) / spent * 1000) / 1024 / 1024).toFixed(2);
console.log('xss(): spent ' + spent + 'ms, ' + speed + 'MB/s');
var x = new xss.FilterXSS();
var timeStart = Date.now();
for (var i = 0; i < COUNT; i++) {
ret = x.process(html);
}
var timeEnd = Date.now();
var spent = timeEnd - timeStart;
var speed = (((html.length * i) / spent * 1000) / 1024 / 1024).toFixed(2);
console.log('xss.process(): spent ' + spent + 'ms, ' + speed + 'MB/s');
var x = new xss.FilterXSS();
var process = x.process.bind(x);
var timeStart = Date.now();
for (var i = 0; i < COUNT; i++) {
ret = process(html);
}
var timeEnd = Date.now();
var spent = timeEnd - timeStart;
var speed = (((html.length * i) / spent * 1000) / 1024 / 1024).toFixed(2);
console.log('xss.process() #2: spent ' + spent + 'ms, ' + speed + 'MB/s');
// 保存结果
//console.log(ret);
fs.writeFileSync(__dirname + '/result.html', ret);

24
benchmark/vs_validator.js Normal file
View File

@@ -0,0 +1,24 @@
/**
* 性能测试 validator模块
*/
var sanitize = require('validator').sanitize;
var fs = require('fs');
var html = fs.readFileSync(__dirname + '/file.html', 'utf8');
var timeStart = Date.now();
for (var i = 0; i < 200; i++) {
var ret = sanitize(html).xss();
}
var timeEnd = Date.now();
//console.log(ret);
fs.writeFileSync(__dirname + '/result_validator.html', ret);
var spent = timeEnd - timeStart;
var speed = (((html.length * i) / spent * 1000) / 1024 / 1024).toFixed(2);
console.log('spent ' + spent + 'ms, ' + speed + 'MB/s');

1
bin/bower_register.cmd Normal file
View File

@@ -0,0 +1 @@
bower register xss git@github.com:leizongmin/js-xss.git

18
bin/build Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
dist_dir="dist";
entrance_file=lib/index.js
output_file=$dist_dir/xss.js
output_mini_file=$dist_dir/xss.min.js
mkdir $dist_dir
echo "browserify..."
node ./node_modules/browserify/bin/cmd.js $entrance_file > $output_file
echo "output $output_file"
echo "minify..."
node ./node_modules/uglify-js/bin/uglifyjs $output_file -o $output_mini_file
echo "output $output_mini_file"
echo "done."

67
bin/xss Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env node
/**
* 命令行工具
*
* @author 老雷<leizongmin@gmail.com>
*/
var fs = require('fs');
var path = require('path');
var program = require('commander');
var xss = require('../');
var packageInfo = require('../package.json');
program
.version(packageInfo.version)
.option('-t, --test', 'active test')
.option('-i, --input <input_file>', 'input file name')
.option('-o, --output <output_file>', 'output filename')
.option('-c, --config <config_file>', 'load custom config')
.option('-s, --strip-ignore-tag', 'set stripIgnoreTag=true')
.option('-b, --strip-ignore-tag-body', 'set stripIgnoreTagBody=true');
program.on('--help', function () {
console.log(' Examples:');
console.log('');
console.log(' $ xss -t');
console.log(' $ xss -i origin.html');
console.log(' $ xss -i origin.html -o targer.html');
console.log(' $ xss -i origin.html -c config.js');
console.log(' $ xss -i origin.html -s');
console.log(' $ xss -i origin.html -s -b');
console.log('');
console.log(' For more details, please see: https://npmjs.org/package/xss')
});
program.parse(process.argv);
if (program.test) {
require('../lib/cli');
return;
}
var config = {};
if (program.config) {
config = require(path.resolve(program.config));
}
if (program.input) {
var input = fs.readFileSync(program.input, 'utf8');
} else {
program.help();
}
if (program['strip-ignore-tag']) {
config.stripIgnoreTag = true;
}
if (program['strip-ignore-tag-body']) {
config.stripIgnoreTagBody = true;
}
var output = xss(input, config);
if (program.output) {
fs.writeFileSync(program.output, output);
} else {
console.log(output);
}

44
bower.json Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "xss",
"version": "0.2.14",
"homepage": "https://github.com/leizongmin/js-xss",
"authors": [
"Zongmin Lei <leizongmin@gmail.com>"
],
"description": "Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist",
"main": "dist/xss.js",
"moduleType": [
"globals",
"node",
"amd"
],
"keywords": [
"sanitization",
"xss",
"sanitize",
"sanitisation",
"input",
"security",
"escape",
"encode",
"filter",
"validator",
"html",
"injection",
"whitelist"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"benchmark",
"bin",
"example",
"build.cmd",
"bower_register.cmd",
"coverage.html"
]
}

4
changelogs.md Normal file
View File

@@ -0,0 +1,4 @@
## v0.2.9
+ `href`属性默认允许`#`开头的值

15
dist/test.html vendored Normal file
View File

@@ -0,0 +1,15 @@
<!doctype html>
<html>
<head>
<title>测试</title>
<meta charset="utf8">
</head>
<body>
<pre id="result"></pre>
</body>
</html>
<script src="xss.js"></script>
<script>
var code = '<script>alert("xss");</' + 'script>';
document.querySelector('#result').innerText = code + '\n被转换成了\n' + filterXSS(code);
</script>

1573
dist/xss.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/xss.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,26 @@
/**
* 应用实例允许标签以data-开头的属性
*
* @author 老雷<leizongmin@gmail.com>
*/
var xss = require('../');
var source = '<div a="1" b="2" data-a="3" data-b="4">hello</div>';
var html = xss(source, {
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
if (name.substr(0, 5) === 'data-') {
// 通过内置的escapeAttrValue函数来对属性值进行转义
return name + '="' + xss.escapeAttrValue(value) + '"';
}
}
});
console.log('%s\nconvert to:\n%s', source, html);
/*
运行结果:
<div a="1" b="2" data-a="3" data-b="4">hello</div>
convert to:
<div data-a="3" data-b="4">hello</div>
*/

View File

@@ -0,0 +1,26 @@
/**
* 应用实例允许名称以x-开头的标签
*
* @author 老雷<leizongmin@gmail.com>
*/
var xss = require('../');
var source = '<x><x-1>he<x-2 checked></x-2>wwww</x-1><a>';
var html = xss(source, {
onIgnoreTag: function (tag, html, options) {
if (tag.substr(0, 2) === 'x-') {
// 不对其属性列表进行过滤
return html;
}
}
});
console.log('%s\nconvert to:\n%s', source, html);
/*
运行结果:
<x><x-1>he<x-2 checked></x-2>wwww</x-1><a>
convert to:
&lt;x&gt;<x-1>he<x-2 checked></x-2>wwww</x-1><a>
*/

View File

@@ -0,0 +1,27 @@
/**
* 应用实例分析HTML代码中的图片列表
*
* @author 老雷<leizongmin@gmail.com>
*/
var xss = require('../');
var source = '<img src="img1">a<img src="img2">b<img src="img3">c<img src="img4">d';
var list = [];
var html = xss(source, {
onTagAttr: function (tag, name, value, isWhiteAttr) {
if (tag === 'img' && name === 'src') {
// 使用内置的friendlyAttrValue函数来对属性值进行转义可将&lt;这类的实体标记转换成打印字符<
list.push(xss.friendlyAttrValue(value));
}
// 不返回任何值,表示还是按照默认的方法处理
}
});
console.log('image list:\n%s', list.join(', '));
/*
运行结果:
image list:
img1, img2, img3, img4
*/

21
example/strip_tag.js Normal file
View File

@@ -0,0 +1,21 @@
/**
* 应用实例去除HTML标签只保留文本内容
*
* @author 老雷<leizongmin@gmail.com>
*/
var xss = require('../');
var source = '<strong>hello</strong><script>alert(/xss/);</script>end';
var html = xss(source, {
whiteList: [], // 白名单为空,表示过滤所有标签
stripIgnoreTag: true, // 过滤所有非白名单标签的HTML
stripIgnoreTagBody: ['script'] // script标签较特殊需要过滤标签中间的内容
});
console.log('text: %s', html);
/*
运行结果:
text: helloend
*/

48
lib/cli.js Normal file
View File

@@ -0,0 +1,48 @@
/**
* 命令行测试工具
*
* @author 老雷<leizongmin@gmail.com>
*/
var xss = require('./');
var readline = require('readline');
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('Enter a blank line to do xss(), enter "@quit" to exit.\n');
function take (c, n) {
var ret = '';
for (var i = 0; i < n; i++) {
ret += c;
}
return ret;
}
function setPrompt (line) {
line = line.toString();
rl.setPrompt('[' + line + ']' + take(' ', 5 - line.length));
rl.prompt();
}
setPrompt(1);
var html = [];
rl.on('line', function (line) {
if (line === '@quit') return process.exit();
if (line === '') {
console.log('');
console.log(xss(html.join('\r\n')));
console.log('');
html = [];
} else {
html.push(line);
}
setPrompt(html.length + 1);
});

413
lib/default.js Normal file
View File

@@ -0,0 +1,413 @@
/**
* 默认配置
*
* @author 老雷<leizongmin@gmail.com>
*/
var FilterCSS = require('cssfilter').FilterCSS;
var getDefaultCSSWhiteList = require('cssfilter').getDefaultWhiteList;
var _ = require('./util');
// 默认白名单
function getDefaultWhiteList () {
return {
a: ['target', 'href', 'title'],
abbr: ['title'],
address: [],
area: ['shape', 'coords', 'href', 'alt'],
article: [],
aside: [],
audio: ['autoplay', 'controls', 'loop', 'preload', 'src'],
b: [],
bdi: ['dir'],
bdo: ['dir'],
big: [],
blockquote: ['cite'],
br: [],
caption: [],
center: [],
cite: [],
code: [],
col: ['align', 'valign', 'span', 'width'],
colgroup: ['align', 'valign', 'span', 'width'],
dd: [],
del: ['datetime'],
details: ['open'],
div: [],
dl: [],
dt: [],
em: [],
font: ['color', 'size', 'face'],
footer: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
header: [],
hr: [],
i: [],
img: ['src', 'alt', 'title', 'width', 'height'],
ins: ['datetime'],
li: [],
mark: [],
nav: [],
ol: [],
p: [],
pre: [],
s: [],
section:[],
small: [],
span: [],
sub: [],
sup: [],
strong: [],
table: ['width', 'border', 'align', 'valign'],
tbody: ['align', 'valign'],
td: ['width', 'rowspan', 'colspan', 'align', 'valign'],
tfoot: ['align', 'valign'],
th: ['width', 'rowspan', 'colspan', 'align', 'valign'],
thead: ['align', 'valign'],
tr: ['rowspan', 'align', 'valign'],
tt: [],
u: [],
ul: [],
video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width']
};
}
// 默认CSS Filter
var defaultCSSFilter = new FilterCSS();
/**
* 匹配到标签时的处理方法
*
* @param {String} tag
* @param {String} html
* @param {Object} options
* @return {String}
*/
function onTag (tag, html, options) {
// do nothing
}
/**
* 匹配到不在白名单上的标签时的处理方法
*
* @param {String} tag
* @param {String} html
* @param {Object} options
* @return {String}
*/
function onIgnoreTag (tag, html, options) {
// do nothing
}
/**
* 匹配到标签属性时的处理方法
*
* @param {String} tag
* @param {String} name
* @param {String} value
* @return {String}
*/
function onTagAttr (tag, name, value) {
// do nothing
}
/**
* 匹配到不在白名单上的标签属性时的处理方法
*
* @param {String} tag
* @param {String} name
* @param {String} value
* @return {String}
*/
function onIgnoreTagAttr (tag, name, value) {
// do nothing
}
/**
* HTML转义
*
* @param {String} html
*/
function escapeHtml (html) {
return html.replace(REGEXP_LT, '&lt;').replace(REGEXP_GT, '&gt;');
}
/**
* 安全的标签属性值
*
* @param {String} tag
* @param {String} name
* @param {String} value
* @param {Object} cssFilter
* @return {String}
*/
function safeAttrValue (tag, name, value, cssFilter) {
// 转换为友好的属性值,再做判断
value = friendlyAttrValue(value);
if (name === 'href' || name === 'src') {
// 过滤 href 和 src 属性
// 仅允许 http:// | https:// | mailto: | / | # 开头的地址
value = _.trim(value);
if (value === '#') return '#';
if (!(value.substr(0, 7) === 'http://' ||
value.substr(0, 8) === 'https://' ||
value.substr(0, 7) === 'mailto:' ||
value[0] === '#' ||
value[0] === '/')) {
return '';
}
} else if (name === 'background') {
// 过滤 background 属性 这个xss漏洞较老了可能已经不适用
// javascript:
REGEXP_DEFAULT_ON_TAG_ATTR_4.lastIndex = 0;
if (REGEXP_DEFAULT_ON_TAG_ATTR_4.test(value)) {
return '';
}
} else if (name === 'style') {
// /*注释*/
/*REGEXP_DEFAULT_ON_TAG_ATTR_3.lastIndex = 0;
if (REGEXP_DEFAULT_ON_TAG_ATTR_3.test(value)) {
return '';
}*/
// expression()
REGEXP_DEFAULT_ON_TAG_ATTR_7.lastIndex = 0;
if (REGEXP_DEFAULT_ON_TAG_ATTR_7.test(value)) {
return '';
}
// url()
REGEXP_DEFAULT_ON_TAG_ATTR_8.lastIndex = 0;
if (REGEXP_DEFAULT_ON_TAG_ATTR_8.test(value)) {
REGEXP_DEFAULT_ON_TAG_ATTR_4.lastIndex = 0;
if (REGEXP_DEFAULT_ON_TAG_ATTR_4.test(value)) {
return '';
}
}
if (cssFilter !== false) {
cssFilter = cssFilter || defaultCSSFilter;
value = cssFilter.process(value);
}
}
// 输出时需要转义<>"
value = escapeAttrValue(value);
return value;
}
// 正则表达式
var REGEXP_LT = /</g;
var REGEXP_GT = />/g;
var REGEXP_QUOTE = /"/g;
var REGEXP_QUOTE_2 = /&quot;/g;
var REGEXP_ATTR_VALUE_1 = /&#([a-zA-Z0-9]*);?/img;
var REGEXP_ATTR_VALUE_COLON = /&colon;?/img;
var REGEXP_ATTR_VALUE_NEWLINE = /&newline;?/img;
var REGEXP_DEFAULT_ON_TAG_ATTR_3 = /\/\*|\*\//mg;
var REGEXP_DEFAULT_ON_TAG_ATTR_4 = /((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a)\:/ig;
var REGEXP_DEFAULT_ON_TAG_ATTR_5 = /^[\s"'`]*(d\s*a\s*t\s*a\s*)\:/ig;
var REGEXP_DEFAULT_ON_TAG_ATTR_6 = /^[\s"'`]*(d\s*a\s*t\s*a\s*)\:\s*image\//ig;
var REGEXP_DEFAULT_ON_TAG_ATTR_7 = /e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/ig;
var REGEXP_DEFAULT_ON_TAG_ATTR_8 = /u\s*r\s*l\s*\(.*/ig;
/**
* 对双引号进行转义
*
* @param {String} str
* @return {String} str
*/
function escapeQuote (str) {
return str.replace(REGEXP_QUOTE, '&quot;');
}
/**
* 对双引号进行转义
*
* @param {String} str
* @return {String} str
*/
function unescapeQuote (str) {
return str.replace(REGEXP_QUOTE_2, '"');
}
/**
* 对html实体编码进行转义
*
* @param {String} str
* @return {String}
*/
function escapeHtmlEntities (str) {
return str.replace(REGEXP_ATTR_VALUE_1, function replaceUnicode (str, code) {
return (code[0] === 'x' || code[0] === 'X')
? String.fromCharCode(parseInt(code.substr(1), 16))
: String.fromCharCode(parseInt(code, 10));
});
}
/**
* 对html5新增的危险实体编码进行转义
*
* @param {String} str
* @return {String}
*/
function escapeDangerHtml5Entities (str) {
return str.replace(REGEXP_ATTR_VALUE_COLON, ':')
.replace(REGEXP_ATTR_VALUE_NEWLINE, ' ');
}
/**
* 清除不可见字符
*
* @param {String} str
* @return {String}
*/
function clearNonPrintableCharacter (str) {
var str2 = '';
for (var i = 0, len = str.length; i < len; i++) {
str2 += str.charCodeAt(i) < 32 ? ' ' : str.charAt(i);
}
return _.trim(str2);
}
/**
* 将标签的属性值转换成一般字符,便于分析
*
* @param {String} str
* @return {String}
*/
function friendlyAttrValue (str) {
str = unescapeQuote(str); // 双引号
str = escapeHtmlEntities(str); // 转换HTML实体编码
str = escapeDangerHtml5Entities(str); // 转换危险的HTML5新增实体编码
str = clearNonPrintableCharacter(str); // 清除不可见字符
return str;
}
/**
* 转义用于输出的标签属性值
*
* @param {String} str
* @return {String}
*/
function escapeAttrValue (str) {
str = escapeQuote(str);
str = escapeHtml(str);
return str;
}
/**
* 去掉不在白名单中的标签onIgnoreTag处理方法
*/
function onIgnoreTagStripAll () {
return '';
}
/**
* 删除标签体
*
* @param {array} tags 要删除的标签列表
* @param {function} next 对不在列表中的标签的处理函数,可选
*/
function StripTagBody (tags, next) {
if (typeof(next) !== 'function') {
next = function () {};
}
var isRemoveAllTag = !Array.isArray(tags);
function isRemoveTag (tag) {
if (isRemoveAllTag) return true;
return (_.indexOf(tags, tag) !== -1);
}
var removeList = []; // 要删除的位置范围列表
var posStart = false; // 当前标签开始位置
return {
onIgnoreTag: function (tag, html, options) {
if (isRemoveTag(tag)) {
if (options.isClosing) {
var ret = '[/removed]';
var end = options.position + ret.length;
removeList.push([posStart !== false ? posStart : options.position, end]);
posStart = false;
return ret;
} else {
if (!posStart) {
posStart = options.position;
}
return '[removed]';
}
} else {
return next(tag, html, options);
}
},
remove: function (html) {
var rethtml = '';
var lastPos = 0;
_.forEach(removeList, function (pos) {
rethtml += html.slice(lastPos, pos[0]);
lastPos = pos[1];
});
rethtml += html.slice(lastPos);
return rethtml;
}
};
}
/**
* 去除备注标签
*
* @param {String} html
* @return {String}
*/
function stripCommentTag (html) {
return html.replace(STRIP_COMMENT_TAG_REGEXP, '');
}
var STRIP_COMMENT_TAG_REGEXP = /<!--[\s\S]*?-->/g;
/**
* 去除不可见字符
*
* @param {String} html
* @return {String}
*/
function stripBlankChar (html) {
var chars = html.split('');
chars = chars.filter(function (char) {
var c = char.charCodeAt(0);
if (c === 127) return false;
if (c <= 31) {
if (c === 10 || c === 13) return true;
return false;
}
return true;
});
return chars.join('');
}
exports.whiteList = getDefaultWhiteList();
exports.getDefaultWhiteList = getDefaultWhiteList;
exports.onTag = onTag;
exports.onIgnoreTag = onIgnoreTag;
exports.onTagAttr = onTagAttr;
exports.onIgnoreTagAttr = onIgnoreTagAttr;
exports.safeAttrValue = safeAttrValue;
exports.escapeHtml = escapeHtml;
exports.escapeQuote = escapeQuote;
exports.unescapeQuote = unescapeQuote;
exports.escapeHtmlEntities = escapeHtmlEntities;
exports.escapeDangerHtml5Entities = escapeDangerHtml5Entities;
exports.clearNonPrintableCharacter = clearNonPrintableCharacter;
exports.friendlyAttrValue = friendlyAttrValue;
exports.escapeAttrValue = escapeAttrValue;
exports.onIgnoreTagStripAll = onIgnoreTagStripAll;
exports.StripTagBody = StripTagBody;
exports.stripCommentTag = stripCommentTag;
exports.stripBlankChar = stripBlankChar;
exports.cssFilter = defaultCSSFilter;
exports.getDefaultCSSWhiteList = getDefaultCSSWhiteList;

35
lib/index.js Normal file
View File

@@ -0,0 +1,35 @@
/**
* 模块入口
*
* @author 老雷<leizongmin@gmail.com>
*/
var DEFAULT = require('./default');
var parser = require('./parser');
var FilterXSS = require('./xss');
/**
* XSS过滤
*
* @param {String} html 要过滤的HTML代码
* @param {Object} options 选项whiteList, onTag, onTagAttr, onIgnoreTag, onIgnoreTagAttr, safeAttrValue, escapeHtml
* @return {String}
*/
function filterXSS (html, options) {
var xss = new FilterXSS(options);
return xss.process(html);
}
// 输出
exports = module.exports = filterXSS;
exports.FilterXSS = FilterXSS;
for (var i in DEFAULT) exports[i] = DEFAULT[i];
for (var i in parser) exports[i] = parser[i];
// 在浏览器端使用
if (typeof window !== 'undefined') {
window.filterXSS = module.exports;
}

237
lib/parser.js Normal file
View File

@@ -0,0 +1,237 @@
/**
* 简单 HTML Parser
*
* @author 老雷<leizongmin@gmail.com>
*/
var _ = require('./util');
/**
* 获取标签的名称
*
* @param {String} html 如:'<a hef="#">'
* @return {String}
*/
function getTagName (html) {
var i = html.indexOf(' ');
if (i === -1) {
var tagName = html.slice(1, -1);
} else {
var tagName = html.slice(1, i + 1);
}
tagName = _.trim(tagName).toLowerCase();
if (tagName.slice(0, 1) === '/') tagName = tagName.slice(1);
if (tagName.slice(-1) === '/') tagName = tagName.slice(0, -1);
return tagName;
}
/**
* 是否为闭合标签
*
* @param {String} html 如:'<a hef="#">'
* @return {Boolean}
*/
function isClosing (html) {
return (html.slice(0, 2) === '</');
}
/**
* 分析HTML代码调用相应的函数处理返回处理后的HTML
*
* @param {String} html
* @param {Function} onTag 处理标签的函数
* 参数格式: function (sourcePosition, position, tag, html, isClosing)
* @param {Function} escapeHtml 对HTML进行转义的函数
* @return {String}
*/
function parseTag (html, onTag, escapeHtml) {
'user strict';
var rethtml = ''; // 待返回的HTML
var lastPos = 0; // 上一个标签结束位置
var tagStart = false; // 当前标签开始位置
var quoteStart = false; // 引号开始位置
var currentPos = 0; // 当前位置
var len = html.length; // HTML长度
var currentHtml = ''; // 当前标签的HTML代码
var currentTagName = ''; // 当前标签的名称
// 逐个分析字符
for (currentPos = 0; currentPos < len; currentPos++) {
var c = html.charAt(currentPos);
if (tagStart === false) {
if (c === '<') {
tagStart = currentPos;
continue;
}
} else {
if (quoteStart === false) {
if (c === '<') {
rethtml += escapeHtml(html.slice(lastPos, currentPos));
tagStart = currentPos;
lastPos = currentPos;
continue;
}
if (c === '>') {
rethtml += escapeHtml(html.slice(lastPos, tagStart));
currentHtml = html.slice(tagStart, currentPos + 1);
currentTagName = getTagName(currentHtml);
rethtml += onTag(tagStart,
rethtml.length,
currentTagName,
currentHtml,
isClosing(currentHtml));
lastPos = currentPos + 1;
tagStart = false;
continue;
}
// HTML标签内的引号仅当前一个字符是等于号时才有效
if ((c === '"' || c === "'") && html.charAt(currentPos - 1) === '=') {
quoteStart = c;
continue;
}
} else {
if (c === quoteStart) {
quoteStart = false;
continue;
}
}
}
}
if (lastPos < html.length) {
rethtml += escapeHtml(html.substr(lastPos));
}
return rethtml;
}
// 不符合属性名称规则的正则表达式
var REGEXP_ATTR_NAME = /[^a-zA-Z0-9_:\.\-]/img;
/**
* 分析标签HTML代码调用相应的函数处理返回HTML
*
* @param {String} html 如标签'<a href="#" target="_blank">' 则为 'href="#" target="_blank"'
* @param {Function} onAttr 处理属性值的函数
* 函数格式: function (name, value)
* @return {String}
*/
function parseAttr (html, onAttr) {
'user strict';
var lastPos = 0; // 当前位置
var retAttrs = []; // 待返回的属性列表
var tmpName = false; // 临时属性名称
var len = html.length; // HTML代码长度
function addAttr (name, value) {
name = _.trim(name);
name = name.replace(REGEXP_ATTR_NAME, '').toLowerCase();
if (name.length < 1) return;
var ret = onAttr(name, value || '');
if (ret) retAttrs.push(ret);
};
// 逐个分析字符
for (var i = 0; i < len; i++) {
var c = html.charAt(i);
var v, j;
if (tmpName === false && c === '=') {
tmpName = html.slice(lastPos, i);
lastPos = i + 1;
continue;
}
if (tmpName !== false) {
// HTML标签内的引号仅当前一个字符是等于号时才有效
if (i === lastPos && (c === '"' || c === "'") && html.charAt(i - 1) === '=') {
j = html.indexOf(c, i + 1);
if (j === -1) {
break;
} else {
v = _.trim(html.slice(lastPos + 1, j));
addAttr(tmpName, v);
tmpName = false;
i = j;
lastPos = i + 1;
continue;
}
}
}
if (c === ' ') {
if (tmpName === false) {
j = findNextEqual(html, i);
if (j === -1) {
v = _.trim(html.slice(lastPos, i));
addAttr(v);
tmpName = false;
lastPos = i + 1;
continue;
} else {
i = j - 1;
continue;
}
} else {
j = findBeforeEqual(html, i - 1);
if (j === -1) {
v = _.trim(html.slice(lastPos, i));
v = stripQuoteWrap(v);
addAttr(tmpName, v);
tmpName = false;
lastPos = i + 1;
continue;
} else {
continue;
}
}
}
}
if (lastPos < html.length) {
if (tmpName === false) {
addAttr(html.slice(lastPos));
} else {
addAttr(tmpName, stripQuoteWrap(_.trim(html.slice(lastPos))));
}
}
return _.trim(retAttrs.join(' '));
}
function findNextEqual (str, i) {
for (; i < str.length; i++) {
var c = str[i];
if (c === ' ') continue;
if (c === '=') return i;
return -1;
}
}
function findBeforeEqual (str, i) {
for (; i > 0; i--) {
var c = str[i];
if (c === ' ') continue;
if (c === '=') return i;
return -1;
}
}
function isQuoteWrapString (text) {
if ((text[0] === '"' && text[text.length - 1] === '"') ||
(text[0] === '\'' && text[text.length - 1] === '\'')) {
return true;
} else {
return false;
}
};
function stripQuoteWrap (text) {
if (isQuoteWrapString(text)) {
return text.substr(1, text.length - 2);
} else {
return text;
}
};
exports.parseTag = parseTag;
exports.parseAttr = parseAttr;

29
lib/util.js Normal file
View File

@@ -0,0 +1,29 @@
module.exports = {
indexOf: function (arr, item) {
var i, j;
if (Array.prototype.indexOf) {
return arr.indexOf(item);
}
for (i = 0, j = arr.length; i < j; i++) {
if (arr[i] === item) {
return i;
}
}
return -1;
},
forEach: function (arr, fn, scope) {
var i, j;
if (Array.prototype.forEach) {
return arr.forEach(fn, scope);
}
for (i = 0, j = arr.length; i < j; i++) {
fn.call(scope, arr[i], i, arr);
}
},
trim: function (str) {
if (String.prototype.trim) {
return str.trim();
}
return str.replace(/(^\s*)|(\s*$)/g, '');
}
};

211
lib/xss.js Normal file
View File

@@ -0,0 +1,211 @@
/**
* 过滤XSS
*
* @author 老雷<leizongmin@gmail.com>
*/
var FilterCSS = require('cssfilter').FilterCSS;
var DEFAULT = require('./default');
var parser = require('./parser');
var parseTag = parser.parseTag;
var parseAttr = parser.parseAttr;
var _ = require('./util');
/**
* 返回值是否为空
*
* @param {Object} obj
* @return {Boolean}
*/
function isNull (obj) {
return (obj === undefined || obj === null);
}
/**
* 取标签内的属性列表字符串
*
* @param {String} html
* @return {Object}
* - {String} html
* - {Boolean} closing
*/
function getAttrs (html) {
var i = html.indexOf(' ');
if (i === -1) {
return {
html: '',
closing: (html[html.length - 2] === '/')
};
}
html = _.trim(html.slice(i + 1, -1));
var isClosing = (html[html.length - 1] === '/');
if (isClosing) html = _.trim(html.slice(0, -1));
return {
html: html,
closing: isClosing
};
}
/**
* 浅拷贝对象
*
* @param {Object} obj
* @return {Object}
*/
function shallowCopyObject (obj) {
var ret = {};
for (var i in obj) {
ret[i] = obj[i];
}
return ret;
}
/**
* XSS过滤对象
*
* @param {Object} options
* 选项whiteList, onTag, onTagAttr, onIgnoreTag,
* onIgnoreTagAttr, safeAttrValue, escapeHtml
* stripIgnoreTagBody, allowCommentTag, stripBlankChar
* css{whiteList, onAttr, onIgnoreAttr} css=false表示禁用cssfilter
*/
function FilterXSS (options) {
options = shallowCopyObject(options || {});
if (options.stripIgnoreTag) {
if (options.onIgnoreTag) {
console.error('Notes: cannot use these two options "stripIgnoreTag" and "onIgnoreTag" at the same time');
}
options.onIgnoreTag = DEFAULT.onIgnoreTagStripAll;
}
options.whiteList = options.whiteList || DEFAULT.whiteList;
options.onTag = options.onTag || DEFAULT.onTag;
options.onTagAttr = options.onTagAttr || DEFAULT.onTagAttr;
options.onIgnoreTag = options.onIgnoreTag || DEFAULT.onIgnoreTag;
options.onIgnoreTagAttr = options.onIgnoreTagAttr || DEFAULT.onIgnoreTagAttr;
options.safeAttrValue = options.safeAttrValue || DEFAULT.safeAttrValue;
options.escapeHtml = options.escapeHtml || DEFAULT.escapeHtml;
this.options = options;
if (options.css === false) {
this.cssFilter = false;
} else {
options.css = options.css || {};
this.cssFilter = new FilterCSS(options.css);
}
}
/**
* 开始处理
*
* @param {String} html
* @return {String}
*/
FilterXSS.prototype.process = function (html) {
// 兼容各种奇葩输入
html = html || '';
html = html.toString();
if (!html) return '';
var me = this;
var options = me.options;
var whiteList = options.whiteList;
var onTag = options.onTag;
var onIgnoreTag = options.onIgnoreTag;
var onTagAttr = options.onTagAttr;
var onIgnoreTagAttr = options.onIgnoreTagAttr;
var safeAttrValue = options.safeAttrValue;
var escapeHtml = options.escapeHtml;
var cssFilter = me.cssFilter;
// 是否清除不可见字符
if (options.stripBlankChar) {
html = DEFAULT.stripBlankChar(html);
}
// 是否禁止备注标签
if (!options.allowCommentTag) {
html = DEFAULT.stripCommentTag(html);
}
// 如果开启了stripIgnoreTagBody
var stripIgnoreTagBody = false;
if (options.stripIgnoreTagBody) {
var stripIgnoreTagBody = DEFAULT.StripTagBody(options.stripIgnoreTagBody, onIgnoreTag);
onIgnoreTag = stripIgnoreTagBody.onIgnoreTag;
}
var retHtml = parseTag(html, function (sourcePosition, position, tag, html, isClosing) {
var info = {
sourcePosition: sourcePosition,
position: position,
isClosing: isClosing,
isWhite: (tag in whiteList)
};
// 调用onTag处理
var ret = onTag(tag, html, info);
if (!isNull(ret)) return ret;
// 默认标签处理方法
if (info.isWhite) {
// 白名单标签,解析标签属性
// 如果是闭合标签,则不需要解析属性
if (info.isClosing) {
return '</' + tag + '>';
}
var attrs = getAttrs(html);
var whiteAttrList = whiteList[tag];
var attrsHtml = parseAttr(attrs.html, function (name, value) {
// 调用onTagAttr处理
var isWhiteAttr = (_.indexOf(whiteAttrList, name) !== -1);
var ret = onTagAttr(tag, name, value, isWhiteAttr);
if (!isNull(ret)) return ret;
// 默认的属性处理方法
if (isWhiteAttr) {
// 白名单属性调用safeAttrValue过滤属性值
value = safeAttrValue(tag, name, value, cssFilter);
if (value) {
return name + '="' + value + '"';
} else {
return name;
}
} else {
// 非白名单属性调用onIgnoreTagAttr处理
var ret = onIgnoreTagAttr(tag, name, value, isWhiteAttr);
if (!isNull(ret)) return ret;
return;
}
});
// 构造新的标签代码
var html = '<' + tag;
if (attrsHtml) html += ' ' + attrsHtml;
if (attrs.closing) html += ' /';
html += '>';
return html;
} else {
// 非白名单标签调用onIgnoreTag处理
var ret = onIgnoreTag(tag, html, info);
if (!isNull(ret)) return ret;
return escapeHtml(html);
}
}, escapeHtml);
// 如果开启了stripIgnoreTagBody需要对结果再进行处理
if (stripIgnoreTagBody) {
retHtml = stripIgnoreTagBody.remove(retHtml);
}
return retHtml;
};
module.exports = FilterXSS;

62
package.json Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "xss",
"main": "./lib/index.js",
"typings": "./typings/xss.d.ts",
"version": "0.3.3",
"description": "Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist",
"author": "Zongmin Lei <leizongmin@gmail.com> (http://ucdok.com)",
"repository": {
"type": "git",
"url": "git://github.com/leizongmin/js-xss.git"
},
"engines": {
"node": ">= 0.10.0"
},
"dependencies": {
"commander": "^2.9.0",
"cssfilter": "0.0.9"
},
"devDependencies": {
"browserify": "^13.0.1",
"coveralls": "^2.11.9",
"debug": "^2.2.0",
"istanbul": "^0.4.3",
"mocha": "^3.0.2",
"uglify-js": "^2.6.1"
},
"files": [
"lib",
"bin/xss",
"dist",
"typings/*.d.ts"
],
"bin": {
"xss": "./bin/xss"
},
"scripts": {
"test": "export DEBUG=xss:* && mocha -t 5000",
"test-cov": "export DEBUG=xss:* && istanbul cover _mocha --report lcovonly -- -t 5000 -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage",
"build": "./bin/build",
"prepublish": "npm run test && npm run build"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/leizongmin/js-xss/issues"
},
"homepage": "https://github.com/leizongmin/js-xss",
"keywords": [
"sanitization",
"xss",
"sanitize",
"sanitisation",
"input",
"security",
"escape",
"encode",
"filter",
"validator",
"html",
"injection",
"whitelist"
]
}

339
test/test_custom_method.js Normal file
View File

@@ -0,0 +1,339 @@
/**
* 测试XSS 自定义处理函数
*
* @author 老雷<leizongmin@gmail.com>
*/
var assert = require('assert');
var xss = require('../');
var debug = require('debug')('xss:test');
describe('test custom XSS method', function () {
it('#onTag - match tag', function () {
var source = 'dd<a href="#"><b><c>haha</c></b></a><br>ff';
var i = 0;
var html = xss(source, {
onTag: function (tag, html, options) {
debug(arguments);
i++;
if (i === 1) {
assert.equal(tag, 'a');
assert.equal(html, '<a href="#">');
assert.equal(options.isClosing, false);
assert.equal(options.position, 2);
assert.equal(options.sourcePosition, 2);
assert.equal(options.isWhite, true);
} else if (i === 2) {
assert.equal(tag, 'b');
assert.equal(html, '<b>');
assert.equal(options.isClosing, false);
assert.equal(options.position, 14);
assert.equal(options.sourcePosition, 14);
assert.equal(options.isWhite, true);
} else if (i === 3) {
assert.equal(tag, 'c');
assert.equal(html, '<c>');
assert.equal(options.isClosing, false);
assert.equal(options.position, 17);
assert.equal(options.sourcePosition, 17);
assert.equal(options.isWhite, false);
} else if (i === 4) {
assert.equal(tag, 'c');
assert.equal(html, '</c>');
assert.equal(options.isClosing, true);
assert.equal(options.position, 30);
assert.equal(options.sourcePosition, 24);
assert.equal(options.isWhite, false);
} else if (i === 5) {
assert.equal(tag, 'b');
assert.equal(html, '</b>');
assert.equal(options.isClosing, true);
assert.equal(options.position, 40);
assert.equal(options.sourcePosition, 28);
assert.equal(options.isWhite, true);
} else if (i === 6) {
assert.equal(tag, 'a');
assert.equal(html, '</a>');
assert.equal(options.isClosing, true);
assert.equal(options.position, 44);
assert.equal(options.sourcePosition, 32);
assert.equal(options.isWhite, true);
} else if (i === 7) {
assert.equal(tag, 'br');
assert.equal(html, '<br>');
assert.equal(options.isClosing, false);
assert.equal(options.position, 48);
assert.equal(options.sourcePosition, 36);
assert.equal(options.isWhite, true);
} else {
throw new Error();
}
}
});
debug(html);
assert.equal(html, 'dd<a href="#"><b>&lt;c&gt;haha&lt;/c&gt;</b></a><br>ff');
});
it('#onTag - return new html', function () {
var source = 'dd<a href="#"><b><c>haha</c></b></a><br>ff';
var i = 0;
var html = xss(source, {
onTag: function (tag, html, options) {
debug(html);
return html;
}
});
debug(html);
assert.equal(html, source);
});
it('#onIgnoreTag - match tag', function () {
var source = 'dd<a href="#"><b><c>haha</c></b></a><br>ff';
var i = 0;
var html = xss(source, {
onIgnoreTag: function (tag, html, options) {
debug(arguments);
i++;
if (i === 1) {
assert.equal(tag, 'c');
assert.equal(html, '<c>');
assert.equal(options.isClosing, false);
assert.equal(options.position, 17);
assert.equal(options.sourcePosition, 17);
assert.equal(options.isWhite, false);
} else if (i === 2) {
assert.equal(tag, 'c');
assert.equal(html, '</c>');
assert.equal(options.isClosing, true);
assert.equal(options.position, 30);
assert.equal(options.sourcePosition, 24);
assert.equal(options.isWhite, false);
} else {
throw new Error();
}
}
});
debug(html);
assert.equal(html, 'dd<a href="#"><b>&lt;c&gt;haha&lt;/c&gt;</b></a><br>ff');
});
it('#onIgnoreTag - return new html', function () {
var source = 'dd<a href="#"><b><c>haha</c></b></a><br>ff';
var i = 0;
var html = xss(source, {
onIgnoreTag: function (tag, html, options) {
debug(html);
return '[' + (options.isClosing ? '/' : '') + 'removed]';
}
});
debug(html);
assert.equal(html, 'dd<a href="#"><b>[removed]haha[/removed]</b></a><br>ff');
});
it('#onTagAttr - match attr', function () {
var source = '<a href="#" target="_blank" checked data-a="b">hi</a href="d">';
var i = 0;
var html = xss(source, {
onTagAttr: function (tag, name, value, isWhiteAttr) {
debug(arguments);
assert.equal(tag, 'a');
i++;
if (i === 1) {
assert.equal(name, 'href');
assert.equal(value, '#');
assert.equal(isWhiteAttr, true);
} else if (i === 2) {
assert.equal(name, 'target');
assert.equal(value, '_blank');
assert.equal(isWhiteAttr, true);
} else if (i === 3) {
assert.equal(name, 'checked');
assert.equal(value, '');
assert.equal(isWhiteAttr, false);
} else if (i === 4) {
assert.equal(name, 'data-a');
assert.equal(value, 'b');
assert.equal(isWhiteAttr, false);
} else {
throw new Error();
}
}
});
debug(html);
assert.equal(html, '<a href="#" target="_blank">hi</a>');
});
it('#onTagAttr - match attr', function () {
var source = '<a href="#" target="_blank" checked data-a="b">hi</a href="d">';
var i = 0;
var html = xss(source, {
onTagAttr: function (tag, name, value, isWhiteAttr) {
debug(arguments);
return '$' + name + '$';
}
});
debug(html);
assert.equal(html, '<a $href$ $target$ $checked$ $data-a$>hi</a>');
});
it('#onIgnoreTagAttr - match attr', function () {
var source = '<a href="#" target="_blank" checked data-a="b">hi</a href="d">';
var i = 0;
var html = xss(source, {
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
debug(arguments);
assert.equal(tag, 'a');
i++;
if (i === 1) {
assert.equal(name, 'checked');
assert.equal(value, '');
assert.equal(isWhiteAttr, false);
} else if (i === 2) {
assert.equal(name, 'data-a');
assert.equal(value, 'b');
assert.equal(isWhiteAttr, false);
} else {
throw new Error();
}
}
});
debug(html);
assert.equal(html, '<a href="#" target="_blank">hi</a>');
});
it('#onIgnoreTagAttr - match attr', function () {
var source = '<a href="#" target="_blank" checked data-a="b">hi</a href="d">';
var i = 0;
var html = xss(source, {
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
debug(arguments);
return '$' + name + '$';
}
});
debug(html);
assert.equal(html, '<a href="#" target="_blank" $checked$ $data-a$>hi</a>');
});
it('#escapeHtml - default', function () {
var source = '<x>yy</x><a>bb</a>';
var html = xss(source);
debug(html);
assert.equal(html, '&lt;x&gt;yy&lt;/x&gt;<a>bb</a>');
});
it('#escapeHtml - return new value', function () {
var source = '<x>yy</x><a>bb</a>';
var html = xss(source, {
escapeHtml: function (str) {
return (str ? '[' + str + ']' : str);
}
});
debug(html);
assert.equal(html, '[<x>][yy][</x>]<a>[bb]</a>');
});
it('#safeAttrValue - default', function () {
var source = '<a href="javascript:alert(/xss/)" title="hi">link</a>';
var html = xss(source);
debug(html);
assert.equal(html, '<a href title="hi">link</a>');
});
it('#safeAttrValue - return new value', function () {
var source = '<a href="javascript:alert(/xss/)" title="hi">link</a>';
var html = xss(source, {
safeAttrValue: function (tag, name, value) {
debug(arguments);
assert.equal(tag, 'a');
return '$' + name + '$';
}
});
debug(html);
assert.equal(html, '<a href="$href$" title="$title$">link</a>');
});
it('#stripIgnoreTag', function () {
var source = '<x>yy</x><a>bb</a>';
var html = xss(source, {
stripIgnoreTag: true
});
debug(html);
assert.equal(html, 'yy<a>bb</a>');
});
it('#stripTagBody - true', function () {
var source = '<a>link</a><x>haha</x><y>a<y></y>b</y>k';
var html = xss(source, {
stripIgnoreTagBody: true
});
debug(html);
assert.equal(html, '<a>link</a>bk');
});
it('#stripIgnoreTagBody - *', function () {
var source = '<a>link</a><x>haha</x><y>a<y></y>b</y>k';
var html = xss(source, {
stripIgnoreTagBody: '*'
});
debug(html);
assert.equal(html, '<a>link</a>bk');
});
it('#stripIgnoreTagBody - [\'x\']', function () {
var source = '<a>link</a><x>haha</x><y>a<y></y>b</y>k';
var html = xss(source, {
stripIgnoreTagBody: ['x']
});
debug(html);
assert.equal(html, '<a>link</a>&lt;y&gt;a&lt;y&gt;&lt;/y&gt;b&lt;/y&gt;k');
});
it('#stripIgnoreTagBody - [\'x\'] & onIgnoreTag', function () {
var source = '<a>link</a><x>haha</x><y>a<y></y>b</y>k';
var html = xss(source, {
stripIgnoreTagBody: ['x'],
onIgnoreTag: function (tag, html, options) {
return '$' + tag + '$';
}
});
debug(html);
assert.equal(html, '<a>link</a>$y$a$y$$y$b$y$k');
});
it('#stripIgnoreTag & stripIgnoreTagBody', function () {
var source = '<scri' + 'pt>alert(/xss/);</scri' + 'pt>';
var html = xss(source, {
stripIgnoreTag: true,
stripIgnoreTagBody: ['script']
});
debug(html);
assert.equal(html, '');
});
it('#stripIgnoreTag & stripIgnoreTagBody - 2', function () {
var source = 'ooxx<scri' + 'pt>alert(/xss/);</scri' + 'pt>';
var html = xss(source, {
stripIgnoreTag: true,
stripIgnoreTagBody: ['script']
});
debug(html);
assert.equal(html, 'ooxx');
});
it('cssFilter', function () {
var whiteList = xss.getDefaultWhiteList();
whiteList.div.push('style');
assert.equal(xss('<div style="width: 50%; vertical-align: top;">hello</div>', { whiteList: whiteList }),
'<div style="width:50%;">hello</div>');
assert.equal(xss('<div style="width: 50%; vertical-align: top;">hello</div>', { whiteList: whiteList, css: false }),
'<div style="width: 50%; vertical-align: top;">hello</div>');
var css = { whiteList: xss.getDefaultCSSWhiteList() };
css.whiteList['vertical-align'] = true;
assert.equal(xss('<div style="width: 50%; vertical-align: top;">hello</div>', { whiteList: whiteList, css: css }),
'<div style="width:50%; vertical-align:top;">hello</div>');
});
});

127
test/test_html_parser.js Normal file
View File

@@ -0,0 +1,127 @@
/**
* 测试 html parser
*
* @author 老雷<leizongmin@gmail.com>
*/
var assert = require('assert');
var parser = require('../lib/parser');
var parseTag = parser.parseTag;
var parseAttr = parser.parseAttr;
var debug = require('debug')('xss:test');
describe('test HTML parser', function () {
function escapeHtml (html) {
return html.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function attr (n, v) {
if (v) {
return n + '="' + v.replace(/"/g, '&quote;') + '"';
} else {
return n;
}
}
it('#parseTag', function () {
var i = 0;
var html = parseTag('hello<A href="#">www</A>ccc<b><br/>', function (sourcePosition, position, tag, html, isClosing) {
i++;
debug(arguments);
if (i === 1) {
// 第1个标签
assert.equal(sourcePosition, 5);
assert.equal(position, 5);
assert.equal(tag, 'a');
assert.equal(html, '<A href="#">');
assert.equal(isClosing, false);
return '[link]';
} else if (i === 2) {
// 第2个标签
assert.equal(sourcePosition, 20);
assert.equal(position, 14);
assert.equal(tag, 'a');
assert.equal(html, '</A>');
assert.equal(isClosing, true);
return '[/link]';
} else if (i === 3) {
// 第3个标签
assert.equal(sourcePosition, 27);
assert.equal(position, 24);
assert.equal(tag, 'b');
assert.equal(html, '<b>');
assert.equal(isClosing, false);
return '[B]';
} else if (i === 4) {
// 第4个标签
assert.equal(sourcePosition, 30);
assert.equal(position, 27);
assert.equal(tag, 'br');
assert.equal(html, '<br/>');
assert.equal(isClosing, false);
return '[BR]';
} else {
throw new Error();
}
}, escapeHtml);
debug(html);
assert.equal(html, 'hello[link]www[/link]ccc[B][BR]');
});
it('#parseAttr', function () {
var i = 0;
var html = parseAttr('href="#"attr1=b attr2=c attr3 attr4=\'value4"\'attr5/', function (name, value) {
i++;
debug(arguments);
if (i === 1) {
assert.equal(name, 'href');
assert.equal(value, '#');
return attr(name, value);
} else if (i === 2) {
assert.equal(name, 'attr1');
assert.equal(value, 'b');
return attr(name, value);
} else if (i === 3) {
assert.equal(name, 'attr2');
assert.equal(value, 'c');
return attr(name, value);
} else if (i === 4) {
assert.equal(name, 'attr3');
assert.equal(value, '');
return attr(name, value);
} else if (i === 5) {
assert.equal(name, 'attr4');
assert.equal(value, 'value4"');
return attr(name, value);
} else if (i === 6) {
assert.equal(name, 'attr5');
assert.equal(value, '');
return attr(name, value);
} else {
throw new Error();
}
});
debug(html);
assert.equal(html, 'href="#" attr1="b" attr2="c" attr3 attr4="value4&quote;" attr5');
});
it('#parseTag & #parseAttr', function () {
var html = parseTag('hi:<a href="#"target=_blank title="this is a link">link</a>', function (sourcePosition, position, tag, html, isClosing) {
if (tag === 'a') {
if (isClosing) return '</a>';
var attrhtml = parseAttr(html.slice(2, -1), function (name, value) {
if (name === 'href' || name === 'target') {
return attr(name, value);
}
});
return '<a ' + attrhtml + '>';
} else {
return escapeHtml(html);
}
}, escapeHtml);
debug(html);
assert.equal(html, 'hi:<a href="#" target="_blank">link</a>');
});
});

250
test/test_xss.js Normal file
View File

@@ -0,0 +1,250 @@
/**
* 测试XSS
*
* @author 老雷<leizongmin@gmail.com>
*/
var assert = require('assert');
var _xss = require('../');
var debug = require('debug')('xss:test');
function xss (html, options) {
debug(JSON.stringify(html));
var ret = _xss(html, options);
debug('\t' + JSON.stringify(ret));
return ret;
}
describe('test XSS', function () {
it('#normal', function () {
// 兼容各种奇葩输入
assert.equal(xss(), '');
assert.equal(xss(null), '');
assert.equal(xss(123), '123');
assert.equal(xss({a: 1111}), '[object Object]');
// 清除不可见字符
assert.equal(xss('a\u0000\u0001\u0002\u0003\r\n b'), 'a\u0000\u0001\u0002\u0003\r\n b');
assert.equal(xss('a\u0000\u0001\u0002\u0003\r\n b', {stripBlankChar: true}), 'a\r\n b');
// 过滤不在白名单的标签
assert.equal(xss('<b>abcd</b>'), '<b>abcd</b>');
assert.equal(xss('<o>abcd</o>'), '&lt;o&gt;abcd&lt;/o&gt;');
assert.equal(xss('<b>abcd</o>'), '<b>abcd&lt;/o&gt;');
assert.equal(xss('<b><o>abcd</b></o>'), '<b>&lt;o&gt;abcd</b>&lt;/o&gt;');
assert.equal(xss('<hr>'), '<hr>');
assert.equal(xss('<xss>'), '&lt;xss&gt;');
assert.equal(xss('<xss o="x">'), '&lt;xss o="x"&gt;');
assert.equal(xss('<a><b>c</b></a>'), '<a><b>c</b></a>');
assert.equal(xss('<a><c>b</c></a>'), '<a>&lt;c&gt;b&lt;/c&gt;</a>');
// 过滤不是标签的<>
assert.equal(xss('<>>'), '&lt;&gt;&gt;');
assert.equal(xss('<scri' + 'pt>'), '&lt;script&gt;');
assert.equal(xss('<<a>b>'), '&lt;<a>b&gt;');
assert.equal(xss('<<<a>>b</a><x>'), '&lt;&lt;<a>&gt;b</a>&lt;x&gt;');
// 过滤不在白名单中的属性
assert.equal(xss('<a oo="1" xx="2" title="3">yy</a>'), '<a title="3">yy</a>');
assert.equal(xss('<a title xx oo>pp</a>'), '<a title>pp</a>');
assert.equal(xss('<a title "">pp</a>'), '<a title>pp</a>');
assert.equal(xss('<a t="">'), '<a>');
// 属性内的特殊字符
assert.equal(xss('<a title="\'<<>>">'), '<a title="\'&lt;&lt;&gt;&gt;">');
assert.equal(xss('<a title=""">'), '<a title>');
assert.equal(xss('<a h=title="oo">'), '<a>');
assert.equal(xss('<a h= title="oo">'), '<a>');
assert.equal(xss('<a title="javascript&colonalert(/xss/)">'), '<a title="javascript:alert(/xss/)">');
assert.equal(xss('<a title"hell aa="fdfd title="ok">hello</a>'), '<a>hello</a>');
// 自动将属性值的单引号转为双引号
assert.equal(xss('<a title=\'abcd\'>'), '<a title="abcd">');
assert.equal(xss('<a title=\'"\'>'), '<a title="&quot;">');
// 没有双引号括起来的属性值
assert.equal(xss('<a title=home>'), '<a title="home">');
assert.equal(xss('<a title=abc("d")>'), '<a title="abc(&quot;d&quot;)">');
assert.equal(xss('<a title=abc(\'d\')>'), '<a title="abc(\'d\')">');
// 单个闭合标签
assert.equal(xss('<img src/>'), '<img src />');
assert.equal(xss('<img src />'), '<img src />');
assert.equal(xss('<img src//>'), '<img src />');
assert.equal(xss('<br/>'), '<br />');
assert.equal(xss('<br />'), '<br />');
// 畸形属性格式
assert.equal(xss('<a target = "_blank" title ="bbb">'), '<a target="_blank" title="bbb">');
assert.equal(xss('<a target = "_blank" title = title = "bbb">'), '<a target="_blank" title="title">');
assert.equal(xss('<img width = 100 height =200 title="xxx">'),
'<img width="100" height="200" title="xxx">');
assert.equal(xss('<img width = 100 height =200 title=xxx>'),
'<img width="100" height="200" title="xxx">');
assert.equal(xss('<img width = 100 height =200 title= xxx>'),
'<img width="100" height="200" title="xxx">');
assert.equal(xss('<img width = 100 height =200 title= "xxx">'),
'<img width="100" height="200" title="xxx">');
assert.equal(xss('<img width = 100 height =200 title= \'xxx\'>'),
'<img width="100" height="200" title="xxx">');
assert.equal(xss('<img width = 100 height =200 title = \'xxx\'>'),
'<img width="100" height="200" title="xxx">');
assert.equal(xss('<img width = 100 height =200 title= "xxx" no=yes alt="yyy">'),
'<img width="100" height="200" title="xxx" alt="yyy">');
assert.equal(xss('<img width = 100 height =200 title= "xxx" no=yes alt="\'yyy\'">'),
'<img width="100" height="200" title="xxx" alt="\'yyy\'">');
});
// 自定义白名单
it('#white list', function () {
// 过滤所有标签
assert.equal(xss('<a title="xx">bb</a>', {whiteList: {}}), '&lt;a title="xx"&gt;bb&lt;/a&gt;');
assert.equal(xss('<hr>', {whiteList: {}}), '&lt;hr&gt;');
// 增加白名单标签及属性
assert.equal(xss('<ooxx yy="ok" cc="no">uu</ooxx>', {whiteList: {ooxx: ['yy']}}), '<ooxx yy="ok">uu</ooxx>');
});
// XSS攻击测试https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
it('#XSS_Filter_Evasion_Cheat_Sheet', function () {
assert.equal(xss('></SCRI' + 'PT>">\'><SCRI' + 'PT>alert(String.fromCharCode(88,83,83))</SCRI' + 'PT>'),
'&gt;&lt;/SCRIPT&gt;"&gt;\'&gt;&lt;SCRIPT&gt;alert(String.fromCharCode(88,83,83))&lt;/SCRIPT&gt;');
assert.equal(xss(';!--"<XSS>=&{()}'), ';!--"&lt;XSS&gt;=&{()}');
assert.equal(xss('<SCRIPT SRC=http://ha.ckers.org/xss.js></SCRI' + 'PT>'),
'&lt;SCRIPT SRC=http://ha.ckers.org/xss.js&gt;&lt;/SCRIPT&gt;');
assert.equal(xss('<IMG SRC="javascript:alert(\'XSS\');">'), '<img src>');
assert.equal(xss('<IMG SRC=javascript:alert(\'XSS\')>'), '<img src>');
assert.equal(xss('<IMG SRC=JaVaScRiPt:alert(\'XSS\')>'), '<img src>');
assert.equal(xss('<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>'), '<img src>');
assert.equal(xss('<IMG """><SCRI' + 'PT>alert("XSS")</SCRI' + 'PT>">'), '<img>&lt;SCRIPT&gt;alert("XSS")&lt;/SCRIPT&gt;"&gt;');
assert.equal(xss('<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>'), '<img src>');
assert.equal(xss('<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>'),
'<img src>');
assert.equal(xss('<IMG SRC=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>'),
'<img src>');
assert.equal(xss('<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>'),
'<img src>');
assert.equal(xss('<IMG SRC="jav ascript:alert(\'XSS\');">'), '<img src>');
assert.equal(xss('<IMG SRC="jav&#x09;ascript:alert(\'XSS\');">'), '<img src>');
assert.equal(xss('<IMG SRC="jav\nascript:alert(\'XSS\');">'), '<img src>');
assert.equal(xss('<IMG SRC=java\0script:alert(\"XSS\")>'), '<img src>');
assert.equal(xss('<IMG SRC=" &#14; javascript:alert(\'XSS\');">'), '<img src>');
assert.equal(xss('<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRI' + 'PT>'),
'&lt;SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"&gt;&lt;/SCRIPT&gt;');
assert.equal(xss('<BODY onload!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>'),
'&lt;BODY onload!#$%&()*~+-_.,:;?@[/|]^`=alert(\"XSS\")&gt;');
assert.equal(xss('<<SCRI' + 'PT>alert("XSS");//<</SCRI' + 'PT>'),
'&lt;&lt;SCRIPT&gt;alert(\"XSS\");//&lt;&lt;/SCRIPT&gt;');
assert.equal(xss('<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >'),
'&lt;SCRIPT SRC=http://ha.ckers.org/xss.js?&lt; B &gt;');
assert.equal(xss('<SCRIPT SRC=//ha.ckers.org/.j'),
'&lt;SCRIPT SRC=//ha.ckers.org/.j');
assert.equal(xss('<IMG SRC="javascript:alert(\'XSS\')"'),
'&lt;IMG SRC=\"javascript:alert(\'XSS\')"');
assert.equal(xss('<iframe src=http://ha.ckers.org/scriptlet.html <'),
'&lt;iframe src=http://ha.ckers.org/scriptlet.html &lt;');
// 过滤 javascript:
assert.equal(xss('<a style="url(\'javascript:alert(1)\')">', {whiteList: {a: ['style']}}), '<a style>');
assert.equal(xss('<td background="url(\'javascript:alert(1)\')">', {whiteList: {td: ['background']}}), '<td background>');
// 过滤 style
assert.equal(xss('<DIV STYLE="width: \nexpression(alert(1));">', {whiteList: {div: ['style']}}), '<div style>');
// 不正常的url
assert.equal(xss('<DIV STYLE="background:\n url (javascript:ooxx);">', {whiteList: {div: ['style']}}), '<div style>');
assert.equal(xss('<DIV STYLE="background:url (javascript:ooxx);">', {whiteList: {div: ['style']}}), '<div style>');
// 正常的url
assert.equal(xss('<DIV STYLE="background: url (ooxx);">', {whiteList: {div: ['style']}}), '<div style="background:url (ooxx);">');
assert.equal(xss('<IMG SRC=\'vbscript:msgbox("XSS")\'>'), '<img src>');
assert.equal(xss('<IMG SRC="livescript:[code]">'), '<img src>');
assert.equal(xss('<IMG SRC="mocha:[code]">'), '<img src>');
assert.equal(xss('<a href="javas/**/cript:alert(\'XSS\');">'), '<a href>');
assert.equal(xss('<a href="javascript">'), '<a href>');
assert.equal(xss('<a href="/javascript/a">'), '<a href="/javascript/a">');
assert.equal(xss('<a href="/javascript/a">'), '<a href="/javascript/a">');
assert.equal(xss('<a href="http://aa.com">'), '<a href="http://aa.com">');
assert.equal(xss('<a href="https://aa.com">'), '<a href="https://aa.com">');
assert.equal(xss('<a href="mailto:me@ucdok.com">'), '<a href="mailto:me@ucdok.com">');
assert.equal(xss('<a href="#hello">'), '<a href="#hello">');
assert.equal(xss('<a href="other">'), '<a href>');
// 这个暂时不知道怎么处理
//assert.equal(xss('¼script¾alert(¢XSS¢)¼/script¾'), '');
assert.equal(xss('<!--[if gte IE 4]><SCRI' + 'PT>alert(\'XSS\');</SCRI' + 'PT><![endif]--> END', {allowCommentTag: true}),
'&lt;!--[if gte IE 4]&gt;&lt;SCRIPT&gt;alert(\'XSS\');&lt;/SCRIPT&gt;&lt;![endif]--&gt; END');
assert.equal(xss('<!--[if gte IE 4]><SCRI' + 'PT>alert(\'XSS\');</SCRI' + 'PT><![endif]--> END'), ' END');
// HTML5新增实体编码 冒号&colon; 换行&NewLine;
assert.equal(xss('<a href="javascript&colon;alert(/xss/)">'), '<a href>');
assert.equal(xss('<a href="javascript&colonalert(/xss/)">'), '<a href>');
assert.equal(xss('<a href="a&NewLine;b">'), '<a href>');
assert.equal(xss('<a href="a&NewLineb">'), '<a href>');
assert.equal(xss('<a href="javasc&NewLine;ript&colon;alert(1)">'), '<a href>');
// data URI 协议过滤
assert.equal(xss('<a href="data:">'), '<a href>');
assert.equal(xss('<a href="d a t a : ">'), '<a href>');
assert.equal(xss('<a href="data: html/text;">'), '<a href>');
assert.equal(xss('<a href="data:html/text;">'), '<a href>');
assert.equal(xss('<a href="data:html /text;">'), '<a href>');
assert.equal(xss('<a href="data: image/text;">'), '<a href>');
assert.equal(xss('<img src="data: aaa/text;">'), '<img src>');
assert.equal(xss('<img src="data:image/png; base64; ofdkofiodiofl">'), '<img src>');
// HTML备注处理
assert.equal(xss('<!-- -->', {allowCommentTag: false}), '');
assert.equal(xss('<!-- a -->', {allowCommentTag: false}), '');
assert.equal(xss('<!--sa -->ss', {allowCommentTag: false}), 'ss');
assert.equal(xss('<!-- ', {allowCommentTag: false}), '&lt;!-- ');
});
it('no options mutated', function () {
var options = {};
var ret = xss('test', options);
console.log(options);
assert.deepEqual(options, {});
var ret2 = new _xss.FilterXSS(options);
console.log(options);
assert.deepEqual(options, {});
});
});

7
typings/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"strictNullChecks": true,
"noImplicitAny": true,
"module": "commonjs"
}
}

49
typings/xss-tests.ts Normal file
View File

@@ -0,0 +1,49 @@
/// <reference path="./xss.d.ts" />
/**
* xss typings test
*
* @author 老雷<leizongmin@gmail.com>
*/
import xss = require('xss');
const x = new xss.FilterXSS();
x.process('html');
const a = xss.StripTagBody([], () => {});
console.log(a.onIgnoreTag, a.remove);
console.log(xss('hello'));
console.log(xss('hello', {
onTag(tag: string, html: string, options: {}): string {
return html;
},
css: false,
}));
xss('hello');
xss('hello', {
escapeHtml(str) {
return str.trim();
},
stripBlankChar: true,
onTag(tag, html, options) {
return html;
},
onIgnoreTag(tag, html) {
},
});
interface ICustomWhiteList extends XSS.IWhiteList {
view?: string[];
}
const whiteList: ICustomWhiteList = xss.getDefaultWhiteList();
console.log(whiteList.abbr);
whiteList.view = [ 'class', 'style', 'id' ];
console.log(whiteList);

150
typings/xss.d.ts vendored Normal file
View File

@@ -0,0 +1,150 @@
/**
* xss
*
* @author 老雷<leizongmin@gmail.com>
*/
declare namespace XSS {
export interface IFilterXSSOptions {
whiteList?: IWhiteList;
onTag?: OnTagHandler;
onTagAttr?: OnTagAttrHandler;
onIgnoreTag?: OnTagHandler;
onIgnoreTagAttr?: OnTagAttrHandler;
safeAttrValue?: SafeAttrValueHandler;
escapeHtml?: EscapeHandler;
stripIgnoreTagBody?: boolean | string[];
allowCommentTag?: boolean;
stripBlankChar?: boolean;
css?: {} | boolean;
}
export interface IWhiteList {
a?: string[];
abbr?: string[];
address?: string[];
area?: string[];
article?: string[];
aside?: string[];
audio?: string[];
b?: string[];
bdi?: string[];
bdo?: string[];
big?: string[];
blockquote?: string[];
br?: string[];
caption?: string[];
center?: string[];
cite?: string[];
code?: string[];
col?: string[];
colgroup?: string[];
dd?: string[];
del?: string[];
details?: string[];
div?: string[];
dl?: string[];
dt?: string[];
em?: string[];
font?: string[];
footer?: string[];
h1?: string[];
h2?: string[];
h3?: string[];
h4?: string[];
h5?: string[];
h6?: string[];
header?: string[];
hr?: string[];
i?: string[];
img?: string[];
ins?: string[];
li?: string[];
mark?: string[];
nav?: string[];
ol?: string[];
p?: string[];
pre?: string[];
s?: string[];
section?: string[];
small?: string[];
span?: string[];
sub?: string[];
sup?: string[];
strong?: string[];
table?: string[];
tbody?: string[];
td?: string[];
tfoot?: string[];
th?: string[];
thead?: string[];
tr?: string[];
tt?: string[];
u?: string[];
ul?: string[];
video?: string[];
}
export type OnTagHandler = (tag: string, html: string, options: {}) => string | void;
export type OnTagAttrHandler = (tag: string, name: string, value: string) => string | void;
export type SafeAttrValueHandler = (tag: string, name: string, value: string, cssFilter: ICSSFilter) => string;
export type EscapeHandler = (str: string) => string;
export interface ICSSFilter {
process(value: string): string;
}
}
declare module 'xss' {
function StripTagBody(tags: string[], next: () => void): {
onIgnoreTag(tag: string, html: string, options: {
position: number;
isClosing: boolean;
}): string;
remove(html: string): string;
};
class FilterXSS {
constructor(options?: XSS.IFilterXSSOptions);
process(html: string): string;
}
interface filterXSS {
(html: string, options?: XSS.IFilterXSSOptions): string;
FilterXSS: typeof FilterXSS;
parseTag(html: string, onTag: (sourcePosition: number, position: number, tag: string, html: string, isClosing: boolean) => string, escapeHtml: XSS.EscapeHandler): string;
parseAttr(html: string, onAttr: (name: string, value: string) => string): string;
whiteList: XSS.IWhiteList;
getDefaultWhiteList(): XSS.IWhiteList;
onTag: XSS.OnTagHandler;
onIgnoreTag: XSS.OnTagHandler;
onTagAttr: XSS.OnTagAttrHandler;
onIgnoreTagAttr: XSS.OnTagAttrHandler;
safeAttrValue: XSS.SafeAttrValueHandler;
escapeHtml: XSS.EscapeHandler;
escapeQuote: XSS.EscapeHandler;
unescapeQuote: XSS.EscapeHandler;
escapeHtmlEntities: XSS.EscapeHandler;
escapeDangerHtml5Entities: XSS.EscapeHandler;
clearNonPrintableCharacter: XSS.EscapeHandler;
friendlyAttrValue: XSS.EscapeHandler;
escapeAttrValue: XSS.EscapeHandler;
onIgnoreTagStripAll(): string;
StripTagBody: typeof StripTagBody;
stripCommentTag: XSS.EscapeHandler;
stripBlankChar: XSS.EscapeHandler;
cssFilter: XSS.ICSSFilter;
getDefaultCSSWhiteList(): XSS.ICSSFilter;
}
var xss: filterXSS;
export = xss;
}