@instructure/platform-sanitize 0.3.2 → 0.3.11
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/dist/index.js +228 -29
- package/dist/sanitizeHtml.d.ts.map +1 -1
- package/dist/sanitizeUrl.d.ts.map +1 -1
- package/package.json +13 -12
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
const
|
|
1
|
+
import c from "dompurify";
|
|
2
|
+
const g = /* @__PURE__ */ new Set([
|
|
3
3
|
// layout
|
|
4
4
|
"display",
|
|
5
5
|
"float",
|
|
@@ -12,6 +12,12 @@ const d = /* @__PURE__ */ new Set([
|
|
|
12
12
|
"direction",
|
|
13
13
|
"user-select",
|
|
14
14
|
"zoom",
|
|
15
|
+
// positioning — values filtered by allowlist hook below
|
|
16
|
+
"position",
|
|
17
|
+
"top",
|
|
18
|
+
"right",
|
|
19
|
+
"bottom",
|
|
20
|
+
"left",
|
|
15
21
|
// sizing
|
|
16
22
|
"width",
|
|
17
23
|
"height",
|
|
@@ -19,6 +25,12 @@ const d = /* @__PURE__ */ new Set([
|
|
|
19
25
|
"min-height",
|
|
20
26
|
"max-width",
|
|
21
27
|
"max-height",
|
|
28
|
+
// box model extensions
|
|
29
|
+
"box-sizing",
|
|
30
|
+
"aspect-ratio",
|
|
31
|
+
"resize",
|
|
32
|
+
"object-fit",
|
|
33
|
+
"object-position",
|
|
22
34
|
// spacing
|
|
23
35
|
"margin",
|
|
24
36
|
"margin-top",
|
|
@@ -46,6 +58,35 @@ const d = /* @__PURE__ */ new Set([
|
|
|
46
58
|
"text-indent",
|
|
47
59
|
"white-space",
|
|
48
60
|
"vertical-align",
|
|
61
|
+
"text-transform",
|
|
62
|
+
"letter-spacing",
|
|
63
|
+
"word-spacing",
|
|
64
|
+
"text-shadow",
|
|
65
|
+
"text-overflow",
|
|
66
|
+
"text-decoration-color",
|
|
67
|
+
"text-decoration-line",
|
|
68
|
+
"text-decoration-style",
|
|
69
|
+
"text-decoration-thickness",
|
|
70
|
+
"text-underline-offset",
|
|
71
|
+
"text-align-last",
|
|
72
|
+
"unicode-bidi",
|
|
73
|
+
// font extensions
|
|
74
|
+
"font-feature-settings",
|
|
75
|
+
"font-kerning",
|
|
76
|
+
"font-optical-sizing",
|
|
77
|
+
"font-variant-caps",
|
|
78
|
+
"font-variant-ligatures",
|
|
79
|
+
"font-variant-numeric",
|
|
80
|
+
// text flow
|
|
81
|
+
"word-break",
|
|
82
|
+
"overflow-wrap",
|
|
83
|
+
"word-wrap",
|
|
84
|
+
"hyphens",
|
|
85
|
+
"line-break",
|
|
86
|
+
"tab-size",
|
|
87
|
+
"writing-mode",
|
|
88
|
+
"text-orientation",
|
|
89
|
+
"text-combine-upright",
|
|
49
90
|
// color & background
|
|
50
91
|
"color",
|
|
51
92
|
"background",
|
|
@@ -56,6 +97,9 @@ const d = /* @__PURE__ */ new Set([
|
|
|
56
97
|
"background-position-x",
|
|
57
98
|
"background-position-y",
|
|
58
99
|
"background-repeat",
|
|
100
|
+
"background-size",
|
|
101
|
+
"background-clip",
|
|
102
|
+
"background-origin",
|
|
59
103
|
// border
|
|
60
104
|
"border",
|
|
61
105
|
"border-color",
|
|
@@ -80,6 +124,12 @@ const d = /* @__PURE__ */ new Set([
|
|
|
80
124
|
"border-left-color",
|
|
81
125
|
"border-left-style",
|
|
82
126
|
"border-left-width",
|
|
127
|
+
// shadows & outline
|
|
128
|
+
"box-shadow",
|
|
129
|
+
"outline",
|
|
130
|
+
"outline-color",
|
|
131
|
+
"outline-style",
|
|
132
|
+
"outline-width",
|
|
83
133
|
// list
|
|
84
134
|
"list-style",
|
|
85
135
|
"list-style-image",
|
|
@@ -87,6 +137,8 @@ const d = /* @__PURE__ */ new Set([
|
|
|
87
137
|
"list-style-type",
|
|
88
138
|
// table
|
|
89
139
|
"table-layout",
|
|
140
|
+
"caption-side",
|
|
141
|
+
"empty-cells",
|
|
90
142
|
// flex
|
|
91
143
|
"flex",
|
|
92
144
|
"flex-basis",
|
|
@@ -126,8 +178,54 @@ const d = /* @__PURE__ */ new Set([
|
|
|
126
178
|
"grid-template",
|
|
127
179
|
"grid-template-areas",
|
|
128
180
|
"grid-template-columns",
|
|
129
|
-
"grid-template-rows"
|
|
130
|
-
|
|
181
|
+
"grid-template-rows",
|
|
182
|
+
// transitions
|
|
183
|
+
"transition",
|
|
184
|
+
"transition-property",
|
|
185
|
+
"transition-duration",
|
|
186
|
+
"transition-timing-function",
|
|
187
|
+
"transition-delay",
|
|
188
|
+
// animations
|
|
189
|
+
"animation",
|
|
190
|
+
"animation-name",
|
|
191
|
+
"animation-duration",
|
|
192
|
+
"animation-timing-function",
|
|
193
|
+
"animation-delay",
|
|
194
|
+
"animation-iteration-count",
|
|
195
|
+
"animation-direction",
|
|
196
|
+
"animation-fill-mode",
|
|
197
|
+
"animation-play-state",
|
|
198
|
+
// columns
|
|
199
|
+
"column-count",
|
|
200
|
+
"column-width",
|
|
201
|
+
"columns",
|
|
202
|
+
"column-rule",
|
|
203
|
+
"column-rule-color",
|
|
204
|
+
"column-rule-style",
|
|
205
|
+
"column-rule-width",
|
|
206
|
+
"column-fill",
|
|
207
|
+
"column-span",
|
|
208
|
+
// page breaks
|
|
209
|
+
"break-before",
|
|
210
|
+
"break-after",
|
|
211
|
+
"break-inside",
|
|
212
|
+
"page-break-before",
|
|
213
|
+
"page-break-after",
|
|
214
|
+
"page-break-inside",
|
|
215
|
+
// generated content
|
|
216
|
+
"quotes",
|
|
217
|
+
"counter-reset",
|
|
218
|
+
"counter-increment",
|
|
219
|
+
"content",
|
|
220
|
+
// UI / interaction
|
|
221
|
+
// pointer-events is included: RCE uses it legitimately (e.g. non-interactive
|
|
222
|
+
// decorative overlays). It is not in the overlay-phishing class because it
|
|
223
|
+
// cannot reposition elements — position/z-index remain blocked.
|
|
224
|
+
"pointer-events",
|
|
225
|
+
"caret-color",
|
|
226
|
+
"accent-color",
|
|
227
|
+
"appearance"
|
|
228
|
+
]), b = /* @__PURE__ */ new Set([
|
|
131
229
|
"src",
|
|
132
230
|
"href",
|
|
133
231
|
"action",
|
|
@@ -138,7 +236,29 @@ const d = /* @__PURE__ */ new Set([
|
|
|
138
236
|
"cite",
|
|
139
237
|
"longdesc",
|
|
140
238
|
"xlink:href"
|
|
141
|
-
]),
|
|
239
|
+
]), u = /^\s*(\/\/|\\)/, m = [
|
|
240
|
+
"background",
|
|
241
|
+
"background-image",
|
|
242
|
+
"list-style",
|
|
243
|
+
"list-style-image",
|
|
244
|
+
"cursor",
|
|
245
|
+
// content: url(...) triggers an HTTP GET even on non-pseudo elements in some
|
|
246
|
+
// browsers; strip it as defense-in-depth against tracking-pixel exfiltration.
|
|
247
|
+
"content"
|
|
248
|
+
], w = /url\s*\(\s*['"]?(?:[a-z][a-z0-9+\-.]*:|\/\/)/i, h = /* @__PURE__ */ new Set([
|
|
249
|
+
"allow-downloads",
|
|
250
|
+
"allow-forms",
|
|
251
|
+
"allow-modals",
|
|
252
|
+
"allow-orientation-lock",
|
|
253
|
+
"allow-pointer-lock",
|
|
254
|
+
"allow-popups",
|
|
255
|
+
"allow-popups-to-escape-sandbox",
|
|
256
|
+
"allow-presentation",
|
|
257
|
+
"allow-same-origin",
|
|
258
|
+
"allow-scripts",
|
|
259
|
+
"allow-storage-access-by-user-activation",
|
|
260
|
+
"allow-top-navigation-by-user-activation"
|
|
261
|
+
]), y = {
|
|
142
262
|
ADD_TAGS: ["iframe"],
|
|
143
263
|
ADD_ATTR: [
|
|
144
264
|
"allowfullscreen",
|
|
@@ -153,7 +273,13 @@ const d = /* @__PURE__ */ new Set([
|
|
|
153
273
|
"target",
|
|
154
274
|
"webkitallowfullscreen",
|
|
155
275
|
"mozallowfullscreen",
|
|
156
|
-
"scrolling"
|
|
276
|
+
"scrolling",
|
|
277
|
+
// MathML 4 (W3C) annotation attributes for screen-reader accessibility.
|
|
278
|
+
// MathCAT, JAWS, and NVDA use `intent` to know how to pronounce
|
|
279
|
+
// expressions; `arg` labels sub-expressions referenced by intent.
|
|
280
|
+
// These are plain string annotations — no URL-loading, no code execution.
|
|
281
|
+
"intent",
|
|
282
|
+
"arg"
|
|
157
283
|
],
|
|
158
284
|
// Rails UJS turns data-method/data-remote/etc. on clickable elements into
|
|
159
285
|
// state-changing requests carrying the victim's CSRF token. Strip them so
|
|
@@ -169,44 +295,117 @@ const d = /* @__PURE__ */ new Set([
|
|
|
169
295
|
// a per-call cast. Do not perform string operations (.slice, .match, etc.)
|
|
170
296
|
// on the return value — those methods do not exist on TrustedHTML and will
|
|
171
297
|
// throw in modern browsers.
|
|
172
|
-
RETURN_TRUSTED_TYPE: !0
|
|
298
|
+
RETURN_TRUSTED_TYPE: !0,
|
|
299
|
+
// Wraps input in a body context during parsing, preventing namespace
|
|
300
|
+
// confusion attacks where fragments like <svg> could influence parse context.
|
|
301
|
+
FORCE_BODY: !0
|
|
173
302
|
};
|
|
174
|
-
let
|
|
175
|
-
function
|
|
176
|
-
return
|
|
303
|
+
let i = null;
|
|
304
|
+
function k() {
|
|
305
|
+
return i || (i = typeof c == "function" ? c(window) : c, i.addHook("afterSanitizeAttributes", (t) => {
|
|
177
306
|
if (!(t instanceof Element) || !t.hasAttribute("style")) return;
|
|
178
|
-
const e = t.style,
|
|
179
|
-
for (let
|
|
180
|
-
const
|
|
181
|
-
|
|
307
|
+
const e = t.style, o = [];
|
|
308
|
+
for (let n = 0; n < e.length; n++) {
|
|
309
|
+
const a = e.item(n);
|
|
310
|
+
g.has(a) || o.push(a);
|
|
311
|
+
}
|
|
312
|
+
for (const n of o) e.removeProperty(n);
|
|
313
|
+
const r = /* @__PURE__ */ new Set([
|
|
314
|
+
"static",
|
|
315
|
+
"relative",
|
|
316
|
+
"absolute",
|
|
317
|
+
"initial",
|
|
318
|
+
"inherit",
|
|
319
|
+
"unset",
|
|
320
|
+
"revert",
|
|
321
|
+
"revert-layer"
|
|
322
|
+
]), s = e.getPropertyValue("position").trim().toLowerCase();
|
|
323
|
+
s && !r.has(s) && e.removeProperty("position");
|
|
324
|
+
for (const n of m) {
|
|
325
|
+
const a = e.getPropertyValue(n);
|
|
326
|
+
a && w.test(a) && e.removeProperty(n);
|
|
182
327
|
}
|
|
183
|
-
for (const r of i) e.removeProperty(r);
|
|
184
328
|
e.length === 0 && t.removeAttribute("style");
|
|
185
|
-
}),
|
|
186
|
-
|
|
187
|
-
}),
|
|
329
|
+
}), i.addHook("uponSanitizeAttribute", (t, e) => {
|
|
330
|
+
b.has(e.attrName) && u.test(e.attrValue) && (e.keepAttr = !1);
|
|
331
|
+
}), i.addHook("afterSanitizeAttributes", (t) => {
|
|
188
332
|
if (!(t instanceof Element) || !t.hasAttribute("srcset")) return;
|
|
189
|
-
(t.getAttribute("srcset") ?? "").split(",").map((r) => r.trim().split(/\s+/)[0]).some((r) =>
|
|
190
|
-
}),
|
|
333
|
+
(t.getAttribute("srcset") ?? "").split(",").map((r) => r.trim().split(/\s+/)[0]).some((r) => u.test(r)) && t.removeAttribute("srcset");
|
|
334
|
+
}), i.addHook("afterSanitizeAttributes", (t) => {
|
|
335
|
+
var r;
|
|
336
|
+
if (!(t instanceof Element) || t.tagName !== "A" && t.tagName !== "AREA" || ((r = t.getAttribute("target")) == null ? void 0 : r.toLowerCase()) !== "_blank") return;
|
|
337
|
+
const e = t.getAttribute("rel") ?? "", o = new Set(e.split(/\s+/).filter(Boolean));
|
|
338
|
+
o.add("noopener"), t.setAttribute("rel", [...o].join(" "));
|
|
339
|
+
}), i.addHook("afterSanitizeAttributes", (t) => {
|
|
340
|
+
if (!(t instanceof Element) || t.tagName !== "IFRAME" || !t.hasAttribute("sandbox")) return;
|
|
341
|
+
const o = (t.getAttribute("sandbox") ?? "").toLowerCase().split(/\s+/).filter(Boolean), r = o.filter((s) => h.has(s));
|
|
342
|
+
r.length !== o.length && t.setAttribute("sandbox", r.join(" "));
|
|
343
|
+
}), i);
|
|
191
344
|
}
|
|
192
|
-
function
|
|
345
|
+
function R(t) {
|
|
193
346
|
if (typeof window > "u")
|
|
194
347
|
throw new Error("sanitizeHtml requires a DOM environment (window is not defined)");
|
|
195
|
-
return
|
|
348
|
+
return k().sanitize(t ?? "", y);
|
|
196
349
|
}
|
|
197
|
-
const
|
|
198
|
-
|
|
350
|
+
const d = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), l = "http://platform-sanitize.invalid/", f = /^\s*\/\//, p = (
|
|
351
|
+
// oxlint-disable-next-line no-control-regex -- intentional security guard
|
|
352
|
+
/^[\u0000-\u0020\u007F-\u00A0\u2000-\u200F\u2028\u2029\u202F\u205F\u2060\u3000\uFEFF]*(?:javascript|data|vbscript|file):/i
|
|
353
|
+
), A = {
|
|
354
|
+
colon: ":",
|
|
355
|
+
// javascript:alert(1) → javascript:alert(1)
|
|
356
|
+
sol: "/",
|
|
357
|
+
// //evil.com → //evil.com (protocol-relative)
|
|
358
|
+
bsol: "\\",
|
|
359
|
+
// \\evil.com → \\evil.com
|
|
360
|
+
Tab: " ",
|
|
361
|
+
// java	script: — WHATWG strips mid-scheme tabs
|
|
362
|
+
NewLine: `
|
|
363
|
+
`
|
|
364
|
+
// java
script:
|
|
365
|
+
};
|
|
366
|
+
function E(t) {
|
|
367
|
+
return t.replace(/&#(\d+);/g, (e, o) => {
|
|
368
|
+
const r = Number(o);
|
|
369
|
+
if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
|
|
370
|
+
try {
|
|
371
|
+
return String.fromCodePoint(r);
|
|
372
|
+
} catch {
|
|
373
|
+
return "";
|
|
374
|
+
}
|
|
375
|
+
}).replace(/&#x([\da-f]+);/gi, (e, o) => {
|
|
376
|
+
const r = parseInt(o, 16);
|
|
377
|
+
if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
|
|
378
|
+
try {
|
|
379
|
+
return String.fromCodePoint(r);
|
|
380
|
+
} catch {
|
|
381
|
+
return "";
|
|
382
|
+
}
|
|
383
|
+
}).replace(/&([A-Za-z][A-Za-z0-9]*);/g, (e, o) => A[o] ?? e);
|
|
384
|
+
}
|
|
385
|
+
function S(t) {
|
|
199
386
|
if (!t || !t.trim()) return "about:blank";
|
|
200
387
|
const e = t.replace(/\\/g, "/");
|
|
201
|
-
if (
|
|
388
|
+
if (f.test(e) || p.test(e)) return "about:blank";
|
|
389
|
+
if (/&[#A-Za-z]/.test(e)) {
|
|
390
|
+
const o = E(e);
|
|
391
|
+
if (f.test(o) || p.test(o))
|
|
392
|
+
return "about:blank";
|
|
393
|
+
try {
|
|
394
|
+
const r = new URL(o, l);
|
|
395
|
+
if (!r.href.startsWith(l) && !d.has(r.protocol))
|
|
396
|
+
return "about:blank";
|
|
397
|
+
} catch {
|
|
398
|
+
return "about:blank";
|
|
399
|
+
}
|
|
400
|
+
}
|
|
202
401
|
try {
|
|
203
|
-
const
|
|
204
|
-
return
|
|
402
|
+
const o = new URL(e, l);
|
|
403
|
+
return !d.has(o.protocol) || (o.protocol === "http:" || o.protocol === "https:") && (o.username || o.password) ? "about:blank" : o.href.startsWith(l) ? t : e.replace(/[\x00-\x1F\u2028\u2029]/g, "").replace(/%250[9ad]/gi, "").replace(/%0[9ad]/gi, "");
|
|
205
404
|
} catch {
|
|
206
405
|
return "about:blank";
|
|
207
406
|
}
|
|
208
407
|
}
|
|
209
408
|
export {
|
|
210
|
-
|
|
211
|
-
|
|
409
|
+
R as sanitizeHtml,
|
|
410
|
+
S as sanitizeUrl
|
|
212
411
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../src/sanitizeHtml.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../src/sanitizeHtml.ts"],"names":[],"mappings":"AA6aA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAKpE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitizeUrl.d.ts","sourceRoot":"","sources":["../src/sanitizeUrl.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sanitizeUrl.d.ts","sourceRoot":"","sources":["../src/sanitizeUrl.ts"],"names":[],"mappings":"AAoFA,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAwDlE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instructure/platform-sanitize",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -17,24 +17,25 @@
|
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "vite build",
|
|
22
|
+
"dev": "vite build --watch",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"test:coverage": "vitest run --coverage",
|
|
26
|
+
"type-check": "tsc --noEmit"
|
|
27
|
+
},
|
|
20
28
|
"peerDependencies": {
|
|
21
|
-
"dompurify": "^3.
|
|
29
|
+
"dompurify": "^3.4.0"
|
|
22
30
|
},
|
|
23
31
|
"devDependencies": {
|
|
32
|
+
"@types/trusted-types": "^2.0.7",
|
|
24
33
|
"@vitest/coverage-v8": "^4.0.17",
|
|
25
|
-
"dompurify": "^3.
|
|
34
|
+
"dompurify": "^3.4.0",
|
|
26
35
|
"jsdom": "^25.0.0",
|
|
27
36
|
"typescript": "^5.3.0",
|
|
28
37
|
"vite": "^6.0.0",
|
|
29
38
|
"vite-plugin-dts": "^4.0.0",
|
|
30
39
|
"vitest": "^4.0.0"
|
|
31
|
-
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"build": "vite build",
|
|
34
|
-
"dev": "vite build --watch",
|
|
35
|
-
"test": "vitest run",
|
|
36
|
-
"test:watch": "vitest",
|
|
37
|
-
"test:coverage": "vitest run --coverage",
|
|
38
|
-
"type-check": "tsc --noEmit"
|
|
39
40
|
}
|
|
40
|
-
}
|
|
41
|
+
}
|