@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 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
@@ -1,4 +1,5 @@
1
1
  import * as xss from 'xss';
2
2
  export interface SanitizeOptions extends xss.IFilterXSSOptions {
3
3
  allowedKeys?: string[];
4
+ preserveCase?: boolean;
4
5
  }
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 sanitizeSvg: (svgContent: string) => string;
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 xss = _interopRequireWildcard(require("xss"));
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 xssInstance.process(data);
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 xssInstance.process(item);
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] = xssInstance.process(item);
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
- const svgWhiteList = {
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 xssInstance = new xss.FilterXSS(svgSanitizeOptions);
123
- return xssInstance.process(svgContent);
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
@@ -1,4 +1,5 @@
1
1
  import * as xss from 'xss';
2
2
  export interface SanitizeOptions extends xss.IFilterXSSOptions {
3
3
  allowedKeys?: string[];
4
+ preserveCase?: boolean;
4
5
  }
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 sanitizeSvg: (svgContent: string) => string;
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 xssInstance.process(data);
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 xssInstance.process(item);
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] = xssInstance.process(item);
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
- const svgWhiteList = {
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 xssInstance = new xss.FilterXSS(svgSanitizeOptions);
137
- return xssInstance.process(svgContent);
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.7",
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": "pnpm test -- --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",