@blocklet/xss 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/index.d.ts +1 -1
- package/cjs/types.d.ts +1 -0
- package/cjs/utils.d.ts +22 -1
- package/cjs/utils.js +59 -10
- package/es/index.d.ts +1 -1
- package/es/types.d.ts +1 -0
- package/es/utils.d.ts +22 -1
- package/es/utils.js +56 -8
- package/package.json +2 -2
package/cjs/index.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ declare const _default: {
|
|
|
8
8
|
name?: string;
|
|
9
9
|
type?: string;
|
|
10
10
|
}) => boolean;
|
|
11
|
-
sanitizeSvg: (svgContent: string) => string;
|
|
11
|
+
sanitizeSvg: (svgContent: string, options?: SanitizeOptions, svgOptions?: import("xss").IFilterXSSOptions) => string;
|
|
12
12
|
};
|
|
13
13
|
export default _default;
|
package/cjs/types.d.ts
CHANGED
package/cjs/utils.d.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
+
import * as xss from 'xss';
|
|
1
2
|
import { SanitizeOptions } from './types';
|
|
2
3
|
export declare const initSanitize: (_options?: SanitizeOptions) => any;
|
|
3
|
-
export declare const
|
|
4
|
+
export declare const svgWhiteList: {
|
|
5
|
+
svg: string[];
|
|
6
|
+
circle: string[];
|
|
7
|
+
ellipse: string[];
|
|
8
|
+
line: string[];
|
|
9
|
+
path: string[];
|
|
10
|
+
polygon: string[];
|
|
11
|
+
polyline: string[];
|
|
12
|
+
rect: string[];
|
|
13
|
+
g: string[];
|
|
14
|
+
text: string[];
|
|
15
|
+
defs: never[];
|
|
16
|
+
clipPath: string[];
|
|
17
|
+
mask: string[];
|
|
18
|
+
use: string[];
|
|
19
|
+
linearGradient: string[];
|
|
20
|
+
radialGradient: string[];
|
|
21
|
+
stop: string[];
|
|
22
|
+
pattern: string[];
|
|
23
|
+
};
|
|
24
|
+
export declare const sanitizeSvg: (svgContent: string, options?: SanitizeOptions, svgOptions?: xss.IFilterXSSOptions) => string;
|
|
4
25
|
export declare const isSvgFile: (svgContent: string, file?: {
|
|
5
26
|
name?: string;
|
|
6
27
|
type?: string;
|
package/cjs/utils.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.sanitizeSvg = exports.isSvgFile = exports.initSanitize = void 0;
|
|
7
|
-
var
|
|
6
|
+
exports.svgWhiteList = exports.sanitizeSvg = exports.isSvgFile = exports.initSanitize = void 0;
|
|
7
|
+
var _xss = _interopRequireWildcard(require("xss"));
|
|
8
|
+
var xss = _xss;
|
|
8
9
|
var _omit = _interopRequireDefault(require("lodash/omit"));
|
|
9
10
|
var _path = _interopRequireDefault(require("path"));
|
|
10
11
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -25,22 +26,32 @@ let defaultOptions = {
|
|
|
25
26
|
return "";
|
|
26
27
|
}
|
|
27
28
|
},
|
|
29
|
+
onTagAttr: (tag, html) => {
|
|
30
|
+
if (tag === "!BACKUP") {
|
|
31
|
+
return html;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
28
34
|
stripIgnoreTagBody: ["script"]
|
|
29
35
|
};
|
|
36
|
+
function advanceProcess(html, options) {
|
|
37
|
+
while (html !== (0, _xss.filterXSS)(html, options)) {
|
|
38
|
+
html = (0, _xss.filterXSS)(html, options);
|
|
39
|
+
}
|
|
40
|
+
return html;
|
|
41
|
+
}
|
|
30
42
|
const initSanitize = (_options = {}) => {
|
|
31
43
|
const options = {
|
|
32
44
|
...defaultOptions,
|
|
33
45
|
..._options
|
|
34
46
|
};
|
|
35
|
-
const xssInstance = new xss.FilterXSS(options);
|
|
36
47
|
const sanitize = data => {
|
|
37
48
|
if (typeof data === "string") {
|
|
38
|
-
return
|
|
49
|
+
return advanceProcess(data, options);
|
|
39
50
|
}
|
|
40
51
|
if (Array.isArray(data)) {
|
|
41
52
|
return data.map(item => {
|
|
42
53
|
if (typeof item === "string") {
|
|
43
|
-
return
|
|
54
|
+
return advanceProcess(item, options);
|
|
44
55
|
}
|
|
45
56
|
if (Array.isArray(item) || typeof item === "object") {
|
|
46
57
|
return sanitize(item);
|
|
@@ -55,7 +66,7 @@ const initSanitize = (_options = {}) => {
|
|
|
55
66
|
}
|
|
56
67
|
const item = data[key];
|
|
57
68
|
if (typeof item === "string") {
|
|
58
|
-
data[key] =
|
|
69
|
+
data[key] = advanceProcess(item, options);
|
|
59
70
|
} else if (Array.isArray(item) || typeof item === "object") {
|
|
60
71
|
data[key] = sanitize(item);
|
|
61
72
|
}
|
|
@@ -66,7 +77,38 @@ const initSanitize = (_options = {}) => {
|
|
|
66
77
|
return sanitize;
|
|
67
78
|
};
|
|
68
79
|
exports.initSanitize = initSanitize;
|
|
69
|
-
|
|
80
|
+
function preserveTagCase(content) {
|
|
81
|
+
const tagCaseMapping = {};
|
|
82
|
+
Object.keys(svgWhiteList).forEach(originalTag => {
|
|
83
|
+
const lowerCaseTag = originalTag.toLowerCase();
|
|
84
|
+
if (lowerCaseTag !== originalTag) {
|
|
85
|
+
tagCaseMapping[lowerCaseTag] = originalTag;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
let result = content;
|
|
89
|
+
Object.entries(tagCaseMapping).forEach(([lowercase, original]) => {
|
|
90
|
+
result = result.replace(new RegExp(`<${lowercase}([^>]*)>`, "gi"), (_match, rest) => `<${original}${rest}>`);
|
|
91
|
+
result = result.replace(new RegExp(`</${lowercase}>`, "gi"), `</${original}>`);
|
|
92
|
+
});
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
function preserveAttrCase(tag, name, value, isWhiteAttr) {
|
|
96
|
+
if (isWhiteAttr) {
|
|
97
|
+
const originalTagKey = Object.keys(svgWhiteList).find(key => key.toLowerCase() === tag.toLowerCase());
|
|
98
|
+
if (originalTagKey) {
|
|
99
|
+
const originalAttrName = svgWhiteList[originalTagKey].find(attr => attr.toLowerCase() === name.toLowerCase());
|
|
100
|
+
if (originalAttrName) {
|
|
101
|
+
value = xss.safeAttrValue(tag, name, value);
|
|
102
|
+
if (value) {
|
|
103
|
+
return originalAttrName + '="' + value + '"';
|
|
104
|
+
} else {
|
|
105
|
+
return originalAttrName;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const svgWhiteList = exports.svgWhiteList = {
|
|
70
112
|
svg: ["width", "height", "viewBox", "xmlns", "version", "preserveAspectRatio", "xml:space"],
|
|
71
113
|
circle: ["cx", "cy", "r", "fill", "stroke", "stroke-width", "fill-opacity", "stroke-opacity"],
|
|
72
114
|
ellipse: ["cx", "cy", "rx", "ry", "fill", "stroke", "stroke-width"],
|
|
@@ -114,13 +156,20 @@ const svgSanitizeOptions = {
|
|
|
114
156
|
}
|
|
115
157
|
}
|
|
116
158
|
};
|
|
117
|
-
const sanitizeSvg = svgContent => {
|
|
159
|
+
const sanitizeSvg = (svgContent, options, svgOptions) => {
|
|
118
160
|
const isSvg = isSvgFile(svgContent);
|
|
119
161
|
if (!isSvg) {
|
|
120
162
|
throw new Error("Invalid SVG content");
|
|
121
163
|
}
|
|
122
|
-
const
|
|
123
|
-
|
|
164
|
+
const filterOptions = {
|
|
165
|
+
...svgSanitizeOptions,
|
|
166
|
+
...(svgOptions || {})
|
|
167
|
+
};
|
|
168
|
+
if (options?.preserveCase) {
|
|
169
|
+
filterOptions.onTagAttr = preserveAttrCase;
|
|
170
|
+
}
|
|
171
|
+
const processedContent = advanceProcess(svgContent, filterOptions);
|
|
172
|
+
return options?.preserveCase ? preserveTagCase(processedContent) : processedContent;
|
|
124
173
|
};
|
|
125
174
|
exports.sanitizeSvg = sanitizeSvg;
|
|
126
175
|
const isSvgFile = (svgContent, file) => {
|
package/es/index.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ declare const _default: {
|
|
|
8
8
|
name?: string;
|
|
9
9
|
type?: string;
|
|
10
10
|
}) => boolean;
|
|
11
|
-
sanitizeSvg: (svgContent: string) => string;
|
|
11
|
+
sanitizeSvg: (svgContent: string, options?: SanitizeOptions, svgOptions?: import("xss").IFilterXSSOptions) => string;
|
|
12
12
|
};
|
|
13
13
|
export default _default;
|
package/es/types.d.ts
CHANGED
package/es/utils.d.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
+
import * as xss from 'xss';
|
|
1
2
|
import { SanitizeOptions } from './types';
|
|
2
3
|
export declare const initSanitize: (_options?: SanitizeOptions) => any;
|
|
3
|
-
export declare const
|
|
4
|
+
export declare const svgWhiteList: {
|
|
5
|
+
svg: string[];
|
|
6
|
+
circle: string[];
|
|
7
|
+
ellipse: string[];
|
|
8
|
+
line: string[];
|
|
9
|
+
path: string[];
|
|
10
|
+
polygon: string[];
|
|
11
|
+
polyline: string[];
|
|
12
|
+
rect: string[];
|
|
13
|
+
g: string[];
|
|
14
|
+
text: string[];
|
|
15
|
+
defs: never[];
|
|
16
|
+
clipPath: string[];
|
|
17
|
+
mask: string[];
|
|
18
|
+
use: string[];
|
|
19
|
+
linearGradient: string[];
|
|
20
|
+
radialGradient: string[];
|
|
21
|
+
stop: string[];
|
|
22
|
+
pattern: string[];
|
|
23
|
+
};
|
|
24
|
+
export declare const sanitizeSvg: (svgContent: string, options?: SanitizeOptions, svgOptions?: xss.IFilterXSSOptions) => string;
|
|
4
25
|
export declare const isSvgFile: (svgContent: string, file?: {
|
|
5
26
|
name?: string;
|
|
6
27
|
type?: string;
|
package/es/utils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as xss from "xss";
|
|
2
|
+
import { filterXSS } from "xss";
|
|
2
3
|
import omit from "lodash/omit";
|
|
3
4
|
import path from "path";
|
|
4
5
|
const ignoreTagList = [
|
|
@@ -40,22 +41,32 @@ let defaultOptions = {
|
|
|
40
41
|
return "";
|
|
41
42
|
}
|
|
42
43
|
},
|
|
44
|
+
onTagAttr: (tag, html) => {
|
|
45
|
+
if (tag === "!BACKUP") {
|
|
46
|
+
return html;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
43
49
|
stripIgnoreTagBody: ["script"]
|
|
44
50
|
};
|
|
51
|
+
function advanceProcess(html, options) {
|
|
52
|
+
while (html !== filterXSS(html, options)) {
|
|
53
|
+
html = filterXSS(html, options);
|
|
54
|
+
}
|
|
55
|
+
return html;
|
|
56
|
+
}
|
|
45
57
|
export const initSanitize = (_options = {}) => {
|
|
46
58
|
const options = {
|
|
47
59
|
...defaultOptions,
|
|
48
60
|
..._options
|
|
49
61
|
};
|
|
50
|
-
const xssInstance = new xss.FilterXSS(options);
|
|
51
62
|
const sanitize = (data) => {
|
|
52
63
|
if (typeof data === "string") {
|
|
53
|
-
return
|
|
64
|
+
return advanceProcess(data, options);
|
|
54
65
|
}
|
|
55
66
|
if (Array.isArray(data)) {
|
|
56
67
|
return data.map((item) => {
|
|
57
68
|
if (typeof item === "string") {
|
|
58
|
-
return
|
|
69
|
+
return advanceProcess(item, options);
|
|
59
70
|
}
|
|
60
71
|
if (Array.isArray(item) || typeof item === "object") {
|
|
61
72
|
return sanitize(item);
|
|
@@ -70,7 +81,7 @@ export const initSanitize = (_options = {}) => {
|
|
|
70
81
|
}
|
|
71
82
|
const item = data[key];
|
|
72
83
|
if (typeof item === "string") {
|
|
73
|
-
data[key] =
|
|
84
|
+
data[key] = advanceProcess(item, options);
|
|
74
85
|
} else if (Array.isArray(item) || typeof item === "object") {
|
|
75
86
|
data[key] = sanitize(item);
|
|
76
87
|
}
|
|
@@ -80,7 +91,40 @@ export const initSanitize = (_options = {}) => {
|
|
|
80
91
|
};
|
|
81
92
|
return sanitize;
|
|
82
93
|
};
|
|
83
|
-
|
|
94
|
+
function preserveTagCase(content) {
|
|
95
|
+
const tagCaseMapping = {};
|
|
96
|
+
Object.keys(svgWhiteList).forEach((originalTag) => {
|
|
97
|
+
const lowerCaseTag = originalTag.toLowerCase();
|
|
98
|
+
if (lowerCaseTag !== originalTag) {
|
|
99
|
+
tagCaseMapping[lowerCaseTag] = originalTag;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
let result = content;
|
|
103
|
+
Object.entries(tagCaseMapping).forEach(([lowercase, original]) => {
|
|
104
|
+
result = result.replace(new RegExp(`<${lowercase}([^>]*)>`, "gi"), (_match, rest) => `<${original}${rest}>`);
|
|
105
|
+
result = result.replace(new RegExp(`</${lowercase}>`, "gi"), `</${original}>`);
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
function preserveAttrCase(tag, name, value, isWhiteAttr) {
|
|
110
|
+
if (isWhiteAttr) {
|
|
111
|
+
const originalTagKey = Object.keys(svgWhiteList).find((key) => key.toLowerCase() === tag.toLowerCase());
|
|
112
|
+
if (originalTagKey) {
|
|
113
|
+
const originalAttrName = svgWhiteList[originalTagKey].find(
|
|
114
|
+
(attr) => attr.toLowerCase() === name.toLowerCase()
|
|
115
|
+
);
|
|
116
|
+
if (originalAttrName) {
|
|
117
|
+
value = xss.safeAttrValue(tag, name, value);
|
|
118
|
+
if (value) {
|
|
119
|
+
return originalAttrName + '="' + value + '"';
|
|
120
|
+
} else {
|
|
121
|
+
return originalAttrName;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export const svgWhiteList = {
|
|
84
128
|
svg: ["width", "height", "viewBox", "xmlns", "version", "preserveAspectRatio", "xml:space"],
|
|
85
129
|
circle: ["cx", "cy", "r", "fill", "stroke", "stroke-width", "fill-opacity", "stroke-opacity"],
|
|
86
130
|
ellipse: ["cx", "cy", "rx", "ry", "fill", "stroke", "stroke-width"],
|
|
@@ -128,13 +172,17 @@ const svgSanitizeOptions = {
|
|
|
128
172
|
}
|
|
129
173
|
}
|
|
130
174
|
};
|
|
131
|
-
export const sanitizeSvg = (svgContent) => {
|
|
175
|
+
export const sanitizeSvg = (svgContent, options, svgOptions) => {
|
|
132
176
|
const isSvg = isSvgFile(svgContent);
|
|
133
177
|
if (!isSvg) {
|
|
134
178
|
throw new Error("Invalid SVG content");
|
|
135
179
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
180
|
+
const filterOptions = { ...svgSanitizeOptions, ...svgOptions || {} };
|
|
181
|
+
if (options?.preserveCase) {
|
|
182
|
+
filterOptions.onTagAttr = preserveAttrCase;
|
|
183
|
+
}
|
|
184
|
+
const processedContent = advanceProcess(svgContent, filterOptions);
|
|
185
|
+
return options?.preserveCase ? preserveTagCase(processedContent) : processedContent;
|
|
138
186
|
};
|
|
139
187
|
export const isSvgFile = (svgContent, file) => {
|
|
140
188
|
if (typeof svgContent !== "string") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/xss",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "blocklet prevent xss attack",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"unbuild": "^2.0.0"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
|
-
"coverage": "
|
|
52
|
+
"coverage": "npm run test -- --coverage",
|
|
53
53
|
"build": "unbuild",
|
|
54
54
|
"build:watch": "npx nodemon --ext 'ts,tsx,json,js,jsx' --exec 'pnpm run build' --ignore 'lib/*' --ignore 'es/*' ",
|
|
55
55
|
"dev": "pnpm run build:watch",
|