Compare commits
10 Commits
352ae5331f
...
66df7c48d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66df7c48d5 | ||
|
|
9c92272047 | ||
|
|
ae15483e9e | ||
|
|
bedb0c09db | ||
|
|
8884b21308 | ||
|
|
4c3c7587f0 | ||
|
|
c339c1f777 | ||
|
|
71c3f25972 | ||
|
|
72844ddc6f | ||
|
|
c2419c4d14 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.0.15 (2024-03-03)
|
||||
|
||||
- [feat: add `<kbd>` to default whitelist](https://github.com/leizongmin/js-xss/pull/279) by @rayrny
|
||||
- [feat: single-quoted attribute value syntax support](https://github.com/leizongmin/js-xss/pull/287) by @mdk000
|
||||
|
||||
## v1.0.14 (2022-08-16)
|
||||
|
||||
- [fix: problem with not closed tag](https://github.com/leizongmin/js-xss/pull/262) by @slawiko
|
||||
- [fix: add allowList to types](https://github.com/leizongmin/js-xss/pull/261) by @metonym
|
||||
|
||||
## v1.0.13 (2022-06-07)
|
||||
|
||||
- [revert: fix: comment has encoded](https://github.com/leizongmin/js-xss/pull/257)
|
||||
|
||||
## v1.0.12 (2022-06-04)
|
||||
|
||||
- [feat: add eslint:recommended check](https://github.com/leizongmin/js-xss/pull/252) by @lumburr
|
||||
|
||||
14
README.md
14
README.md
@@ -280,6 +280,20 @@ function safeAttrValue(tag, name, value) {
|
||||
}
|
||||
```
|
||||
|
||||
### Customize output attribute value syntax for HTML
|
||||
|
||||
By specifying a `singleQuotedAttributeValue`. Use `true` for `'`. Otherwise default `"` will be used
|
||||
|
||||
```javascript
|
||||
var options = {
|
||||
singleQuotedAttributeValue: true,
|
||||
};
|
||||
// With the configuration specified above, the following HTML:
|
||||
// <a href="#">Hello</a>
|
||||
// would become:
|
||||
// <a href='#'>Hello</a>
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
@@ -240,7 +240,7 @@ function onIgnoreTag(tag, html, options) {
|
||||
function onIgnoreTagAttr(tag, name, value, isWhiteAttr) {
|
||||
// 参数说明与onTagAttr相同
|
||||
// 如果返回一个字符串,则当前属性值将被替换为该字符串
|
||||
// 如果不返回任何值,则使用默认的处理方法(删除该属)
|
||||
// 如果不返回任何值,则使用默认的处理方法(删除该属性)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
27
dist/xss.js
vendored
27
dist/xss.js
vendored
@@ -58,8 +58,9 @@ function getDefaultWhiteList() {
|
||||
header: [],
|
||||
hr: [],
|
||||
i: [],
|
||||
img: ["src", "alt", "title", "width", "height"],
|
||||
img: ["src", "alt", "title", "width", "height", "loading"],
|
||||
ins: ["datetime"],
|
||||
kbd: [],
|
||||
li: [],
|
||||
mark: [],
|
||||
nav: [],
|
||||
@@ -160,15 +161,6 @@ function escapeHtml(html) {
|
||||
return html.replace(REGEXP_LT, "<").replace(REGEXP_GT, ">");
|
||||
}
|
||||
|
||||
/**
|
||||
* default escapeHtml function but dont escape comment
|
||||
*
|
||||
* @param {String} html
|
||||
*/
|
||||
function escapeHtmlNotComment(html) {
|
||||
return html.replace(REGEXP_LT_NOT_COMMENT, "<").replace(REGEXP_RT_NOT_COMMENT, ">");
|
||||
}
|
||||
|
||||
/**
|
||||
* default safeAttrValue function
|
||||
*
|
||||
@@ -238,8 +230,6 @@ function safeAttrValue(tag, name, value, cssFilter) {
|
||||
// RegExp list
|
||||
var REGEXP_LT = /</g;
|
||||
var REGEXP_GT = />/g;
|
||||
var REGEXP_LT_NOT_COMMENT = /<(?!!--)/g;
|
||||
var REGEXP_RT_NOT_COMMENT = /(?<!--)>/g;
|
||||
var REGEXP_QUOTE = /"/g;
|
||||
var REGEXP_QUOTE_2 = /"/g;
|
||||
var REGEXP_ATTR_VALUE_1 = /&#([a-zA-Z0-9]*);?/gim;
|
||||
@@ -456,7 +446,6 @@ exports.onTagAttr = onTagAttr;
|
||||
exports.onIgnoreTagAttr = onIgnoreTagAttr;
|
||||
exports.safeAttrValue = safeAttrValue;
|
||||
exports.escapeHtml = escapeHtml;
|
||||
exports.escapeHtmlNotComment = escapeHtmlNotComment;
|
||||
exports.escapeQuote = escapeQuote;
|
||||
exports.unescapeQuote = unescapeQuote;
|
||||
exports.escapeHtmlEntities = escapeHtmlEntities;
|
||||
@@ -468,6 +457,7 @@ exports.onIgnoreTagStripAll = onIgnoreTagStripAll;
|
||||
exports.StripTagBody = StripTagBody;
|
||||
exports.stripCommentTag = stripCommentTag;
|
||||
exports.stripBlankChar = stripBlankChar;
|
||||
exports.attributeWrapSign = '"';
|
||||
exports.cssFilter = defaultCSSFilter;
|
||||
exports.getDefaultCSSWhiteList = getDefaultCSSWhiteList;
|
||||
|
||||
@@ -598,7 +588,7 @@ function parseTag(html, onTag, escapeHtml) {
|
||||
lastPos = currentPos;
|
||||
continue;
|
||||
}
|
||||
if (c === ">") {
|
||||
if (c === ">" || currentPos === len - 1) {
|
||||
rethtml += escapeHtml(html.slice(lastPos, tagStart));
|
||||
currentHtml = html.slice(tagStart, currentPos + 1);
|
||||
currentTagName = getTagName(currentHtml);
|
||||
@@ -633,7 +623,7 @@ function parseTag(html, onTag, escapeHtml) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastPos < html.length) {
|
||||
if (lastPos < len) {
|
||||
rethtml += escapeHtml(html.substr(lastPos));
|
||||
}
|
||||
|
||||
@@ -922,12 +912,14 @@ function FilterXSS(options) {
|
||||
options.whiteList = DEFAULT.whiteList;
|
||||
}
|
||||
|
||||
this.attributeWrapSign = options.singleQuotedAttributeValue === true ? "'" : DEFAULT.attributeWrapSign;
|
||||
|
||||
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 || (options.allowCommentTag ? DEFAULT.escapeHtmlNotComment : DEFAULT.escapeHtml);
|
||||
options.escapeHtml = options.escapeHtml || DEFAULT.escapeHtml;
|
||||
this.options = options;
|
||||
|
||||
if (options.css === false) {
|
||||
@@ -959,6 +951,7 @@ FilterXSS.prototype.process = function (html) {
|
||||
var onIgnoreTagAttr = options.onIgnoreTagAttr;
|
||||
var safeAttrValue = options.safeAttrValue;
|
||||
var escapeHtml = options.escapeHtml;
|
||||
var attributeWrapSign = me.attributeWrapSign;
|
||||
var cssFilter = me.cssFilter;
|
||||
|
||||
// remove invisible characters
|
||||
@@ -1012,7 +1005,7 @@ FilterXSS.prototype.process = function (html) {
|
||||
// call `safeAttrValue()`
|
||||
value = safeAttrValue(tag, name, value, cssFilter);
|
||||
if (value) {
|
||||
return name + '="' + value + '"';
|
||||
return name + '=' + attributeWrapSign + value + attributeWrapSign;
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
|
||||
2
dist/xss.min.js
vendored
2
dist/xss.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -57,8 +57,9 @@ function getDefaultWhiteList() {
|
||||
header: [],
|
||||
hr: [],
|
||||
i: [],
|
||||
img: ["src", "alt", "title", "width", "height"],
|
||||
img: ["src", "alt", "title", "width", "height", "loading"],
|
||||
ins: ["datetime"],
|
||||
kbd: [],
|
||||
li: [],
|
||||
mark: [],
|
||||
nav: [],
|
||||
@@ -455,5 +456,6 @@ exports.onIgnoreTagStripAll = onIgnoreTagStripAll;
|
||||
exports.StripTagBody = StripTagBody;
|
||||
exports.stripCommentTag = stripCommentTag;
|
||||
exports.stripBlankChar = stripBlankChar;
|
||||
exports.attributeWrapSign = '"';
|
||||
exports.cssFilter = defaultCSSFilter;
|
||||
exports.getDefaultCSSWhiteList = getDefaultCSSWhiteList;
|
||||
|
||||
@@ -71,7 +71,7 @@ function parseTag(html, onTag, escapeHtml) {
|
||||
lastPos = currentPos;
|
||||
continue;
|
||||
}
|
||||
if (c === ">") {
|
||||
if (c === ">" || currentPos === len - 1) {
|
||||
rethtml += escapeHtml(html.slice(lastPos, tagStart));
|
||||
currentHtml = html.slice(tagStart, currentPos + 1);
|
||||
currentTagName = getTagName(currentHtml);
|
||||
@@ -106,7 +106,7 @@ function parseTag(html, onTag, escapeHtml) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastPos < html.length) {
|
||||
if (lastPos < len) {
|
||||
rethtml += escapeHtml(html.substr(lastPos));
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,8 @@ function FilterXSS(options) {
|
||||
options.whiteList = DEFAULT.whiteList;
|
||||
}
|
||||
|
||||
this.attributeWrapSign = options.singleQuotedAttributeValue === true ? "'" : DEFAULT.attributeWrapSign;
|
||||
|
||||
options.onTag = options.onTag || DEFAULT.onTag;
|
||||
options.onTagAttr = options.onTagAttr || DEFAULT.onTagAttr;
|
||||
options.onIgnoreTag = options.onIgnoreTag || DEFAULT.onIgnoreTag;
|
||||
@@ -137,6 +139,7 @@ FilterXSS.prototype.process = function (html) {
|
||||
var onIgnoreTagAttr = options.onIgnoreTagAttr;
|
||||
var safeAttrValue = options.safeAttrValue;
|
||||
var escapeHtml = options.escapeHtml;
|
||||
var attributeWrapSign = me.attributeWrapSign;
|
||||
var cssFilter = me.cssFilter;
|
||||
|
||||
// remove invisible characters
|
||||
@@ -190,7 +193,7 @@ FilterXSS.prototype.process = function (html) {
|
||||
// call `safeAttrValue()`
|
||||
value = safeAttrValue(tag, name, value, cssFilter);
|
||||
if (value) {
|
||||
return name + '="' + value + '"';
|
||||
return name + '=' + attributeWrapSign + value + attributeWrapSign;
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "xss",
|
||||
"main": "./lib/index.js",
|
||||
"typings": "./typings/xss.d.ts",
|
||||
"version": "1.0.12",
|
||||
"version": "1.0.15",
|
||||
"description": "Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist",
|
||||
"author": "Zongmin Lei <leizongmin@gmail.com> (http://ucdok.com)",
|
||||
"repository": {
|
||||
|
||||
@@ -88,6 +88,7 @@ describe("test XSS", function() {
|
||||
assert.equal(xss("<img src//>"), "<img src />");
|
||||
assert.equal(xss("<br/>"), "<br />");
|
||||
assert.equal(xss("<br />"), "<br />");
|
||||
assert.equal(xss("<img src=x onerror=alert('XSS')"), "<img src>");
|
||||
|
||||
// 畸形属性格式
|
||||
assert.equal(
|
||||
@@ -132,6 +133,12 @@ describe("test XSS", function() {
|
||||
),
|
||||
'<img width="100" height="200" title="xxx" alt="\'yyy\'">'
|
||||
);
|
||||
assert.equal(
|
||||
xss(
|
||||
'<img loading="lazy">'
|
||||
),
|
||||
'<img loading="lazy">'
|
||||
);
|
||||
|
||||
// 使用Tab或换行符分隔的属性
|
||||
assert.equal(
|
||||
@@ -421,6 +428,22 @@ describe("test XSS", function() {
|
||||
);
|
||||
});
|
||||
|
||||
it("#singleQuotedAttributeValue", function() {
|
||||
assert.equal(xss('<a title="xx">not-defined</a>'), '<a title="xx">not-defined</a>');
|
||||
assert.equal(
|
||||
xss('<a title="xx">single-quoted</a>', { singleQuotedAttributeValue: true }),
|
||||
'<a title=\'xx\'>single-quoted</a>'
|
||||
);
|
||||
assert.equal(
|
||||
xss('<a title="xx">double-quoted</a>', { singleQuotedAttributeValue: false }),
|
||||
'<a title="xx">double-quoted</a>'
|
||||
);
|
||||
assert.equal(
|
||||
xss('<a title="xx">invalid-value</a>', { singleQuotedAttributeValue: 'invalid' }),
|
||||
'<a title="xx">invalid-value</a>'
|
||||
);
|
||||
})
|
||||
|
||||
it("no options mutated", function() {
|
||||
var options = {};
|
||||
|
||||
|
||||
3
typings/xss.d.ts
vendored
3
typings/xss.d.ts
vendored
@@ -10,6 +10,7 @@ declare module "xss" {
|
||||
|
||||
namespace XSS {
|
||||
export interface IFilterXSSOptions {
|
||||
allowList?: IWhiteList;
|
||||
whiteList?: IWhiteList;
|
||||
onTag?: OnTagHandler;
|
||||
onTagAttr?: OnTagAttrHandler;
|
||||
@@ -21,6 +22,7 @@ declare module "xss" {
|
||||
stripIgnoreTagBody?: boolean | string[];
|
||||
allowCommentTag?: boolean;
|
||||
stripBlankChar?: boolean;
|
||||
singleQuotedAttributeValue?: boolean;
|
||||
css?: {} | boolean;
|
||||
}
|
||||
|
||||
@@ -194,6 +196,7 @@ declare module "xss" {
|
||||
export function onIgnoreTagStripAll(): string;
|
||||
export const stripCommentTag: EscapeHandler;
|
||||
export const stripBlankChar: EscapeHandler;
|
||||
export const attributeWrapSign: string;
|
||||
export const cssFilter: ICSSFilter;
|
||||
export function getDefaultCSSWhiteList(): ICSSFilter;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user