@instructure/platform-sanitize 0.5.0 → 0.5.2
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 +137 -99
- package/dist/sanitizeHtml.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
|
-
import
|
|
2
|
-
const
|
|
1
|
+
import m from "dompurify";
|
|
2
|
+
const f = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), u = "http://platform-sanitize.invalid/", p = /^\s*\/\//, b = (
|
|
3
|
+
// oxlint-disable-next-line no-control-regex -- intentional security guard
|
|
4
|
+
/^[\u0000-\u0020\u007F-\u00A0\u2000-\u200F\u2028\u2029\u202F\u205F\u2060\u3000\uFEFF]*(?:javascript|data|vbscript|file):/i
|
|
5
|
+
), h = {
|
|
6
|
+
colon: ":",
|
|
7
|
+
// javascript:alert(1) → javascript:alert(1)
|
|
8
|
+
sol: "/",
|
|
9
|
+
// //evil.com → //evil.com (protocol-relative)
|
|
10
|
+
bsol: "\\",
|
|
11
|
+
// \\evil.com → \\evil.com
|
|
12
|
+
Tab: " ",
|
|
13
|
+
// java	script: — WHATWG strips mid-scheme tabs
|
|
14
|
+
NewLine: `
|
|
15
|
+
`
|
|
16
|
+
// java
script:
|
|
17
|
+
};
|
|
18
|
+
function w(s) {
|
|
19
|
+
return s.replace(/&#(\d+);/g, (t, e) => {
|
|
20
|
+
const r = Number(e);
|
|
21
|
+
if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
|
|
22
|
+
try {
|
|
23
|
+
return String.fromCodePoint(r);
|
|
24
|
+
} catch {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}).replace(/&#x([\da-f]+);/gi, (t, e) => {
|
|
28
|
+
const r = parseInt(e, 16);
|
|
29
|
+
if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
|
|
30
|
+
try {
|
|
31
|
+
return String.fromCodePoint(r);
|
|
32
|
+
} catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}).replace(/&([A-Za-z][A-Za-z0-9]*);/g, (t, e) => h[e] ?? t);
|
|
36
|
+
}
|
|
37
|
+
function A(s) {
|
|
38
|
+
if (!s || !s.trim()) return "about:blank";
|
|
39
|
+
const t = s.replace(/\\/g, "/");
|
|
40
|
+
if (p.test(t) || b.test(t)) return "about:blank";
|
|
41
|
+
if (/&[#A-Za-z]/.test(t)) {
|
|
42
|
+
const e = w(t);
|
|
43
|
+
if (p.test(e) || b.test(e))
|
|
44
|
+
return "about:blank";
|
|
45
|
+
try {
|
|
46
|
+
const r = new URL(e, u);
|
|
47
|
+
if (!r.href.startsWith(u) && !f.has(r.protocol))
|
|
48
|
+
return "about:blank";
|
|
49
|
+
} catch {
|
|
50
|
+
return "about:blank";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const e = new URL(t, u);
|
|
55
|
+
return !f.has(e.protocol) || (e.protocol === "http:" || e.protocol === "https:") && (e.username || e.password) ? "about:blank" : e.href.startsWith(u) ? s : t.replace(/[\x00-\x1F\u2028\u2029]/g, "").replace(/%250[9ad]/gi, "").replace(/%0[9ad]/gi, "");
|
|
56
|
+
} catch {
|
|
57
|
+
return "about:blank";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const y = /* @__PURE__ */ new Set([
|
|
3
61
|
// layout
|
|
4
62
|
"display",
|
|
5
63
|
"float",
|
|
@@ -241,7 +299,7 @@ const w = /* @__PURE__ */ new Set([
|
|
|
241
299
|
"caret-color",
|
|
242
300
|
"accent-color",
|
|
243
301
|
"appearance"
|
|
244
|
-
]),
|
|
302
|
+
]), k = /* @__PURE__ */ new Set([
|
|
245
303
|
"src",
|
|
246
304
|
"href",
|
|
247
305
|
"action",
|
|
@@ -252,7 +310,7 @@ const w = /* @__PURE__ */ new Set([
|
|
|
252
310
|
"cite",
|
|
253
311
|
"longdesc",
|
|
254
312
|
"xlink:href"
|
|
255
|
-
]),
|
|
313
|
+
]), S = ["content"], E = /url\s*\(\s*['"]?(?:[a-z][a-z0-9+\-.]*:|\/\/)/i, v = /* @__PURE__ */ new Set([
|
|
256
314
|
"allow-downloads",
|
|
257
315
|
"allow-forms",
|
|
258
316
|
"allow-modals",
|
|
@@ -265,7 +323,14 @@ const w = /* @__PURE__ */ new Set([
|
|
|
265
323
|
"allow-scripts",
|
|
266
324
|
"allow-storage-access-by-user-activation",
|
|
267
325
|
"allow-top-navigation-by-user-activation"
|
|
268
|
-
]),
|
|
326
|
+
]), x = {
|
|
327
|
+
"data-url": "data-custom-url",
|
|
328
|
+
"data-method": "data-custom-method",
|
|
329
|
+
"data-remote": "data-custom-remote",
|
|
330
|
+
"data-remove": "data-custom-remove",
|
|
331
|
+
"data-confirm": "data-custom-confirm",
|
|
332
|
+
"data-disable-with": "data-custom-disable-with"
|
|
333
|
+
}, _ = /* @__PURE__ */ new Set(["data-item-href"]), g = {
|
|
269
334
|
// semantics and annotation are re-allowed because DOMPurify blocks all of
|
|
270
335
|
// mathMlDisallowed by default. semantics is a safe structural wrapper;
|
|
271
336
|
// annotation carries LaTeX source text (e.g. \frac{1}{2}); Canvas emits
|
|
@@ -299,7 +364,33 @@ const w = /* @__PURE__ */ new Set([
|
|
|
299
364
|
// files from a separate sandboxed origin. object/embed also have no usable
|
|
300
365
|
// sandbox attribute, so the sandbox-token hook cannot constrain them — again,
|
|
301
366
|
// parity with an unsandboxed iframe, which is already permitted.
|
|
302
|
-
|
|
367
|
+
//
|
|
368
|
+
// maction…none are the remaining MathML presentation/layout containers from
|
|
369
|
+
// DOMPurify's mathMlDisallowed set (RCX-5259). They are re-added as tags only:
|
|
370
|
+
// unlike annotation-xml they are NOT HTML integration points, so they carry no
|
|
371
|
+
// namespace-confusion (mXSS) risk and need no guarding hook. Because they go in
|
|
372
|
+
// ADD_TAGS and not ADD_ATTR, DOMPurify keeps filtering their attributes — an
|
|
373
|
+
// event handler like onclick on <maction> is still stripped.
|
|
374
|
+
ADD_TAGS: [
|
|
375
|
+
"iframe",
|
|
376
|
+
"semantics",
|
|
377
|
+
"annotation",
|
|
378
|
+
"annotation-xml",
|
|
379
|
+
"object",
|
|
380
|
+
"embed",
|
|
381
|
+
"param",
|
|
382
|
+
"maction",
|
|
383
|
+
"maligngroup",
|
|
384
|
+
"malignmark",
|
|
385
|
+
"mlongdiv",
|
|
386
|
+
"mscarries",
|
|
387
|
+
"mscarry",
|
|
388
|
+
"msgroup",
|
|
389
|
+
"mstack",
|
|
390
|
+
"msline",
|
|
391
|
+
"msrow",
|
|
392
|
+
"none"
|
|
393
|
+
],
|
|
303
394
|
ADD_ATTR: [
|
|
304
395
|
"allowfullscreen",
|
|
305
396
|
"allow",
|
|
@@ -330,10 +421,6 @@ const w = /* @__PURE__ */ new Set([
|
|
|
330
421
|
"intent",
|
|
331
422
|
"arg"
|
|
332
423
|
],
|
|
333
|
-
// Rails UJS turns data-method/data-remote/etc. on clickable elements into
|
|
334
|
-
// state-changing requests carrying the victim's CSRF token. Strip them so
|
|
335
|
-
// user-authored HTML cannot become a CSRF gadget when UJS is on the page.
|
|
336
|
-
FORBID_ATTR: ["data-method", "data-remote", "data-url", "data-confirm", "data-disable-with"],
|
|
337
424
|
// In browsers that support the Trusted Types API (Chromium, Firefox),
|
|
338
425
|
// DOMPurify returns a TrustedHTML object rather than a plain string. The
|
|
339
426
|
// exported sanitizeHtml signature declares string — a deliberate lie that
|
|
@@ -349,18 +436,18 @@ const w = /* @__PURE__ */ new Set([
|
|
|
349
436
|
// confusion attacks where fragments like <svg> could influence parse context.
|
|
350
437
|
FORCE_BODY: !0
|
|
351
438
|
};
|
|
352
|
-
let
|
|
353
|
-
function
|
|
354
|
-
if (
|
|
355
|
-
|
|
439
|
+
let a = null;
|
|
440
|
+
function R() {
|
|
441
|
+
if (a) return a;
|
|
442
|
+
a = typeof m == "function" ? m(window) : m, a.addHook("afterSanitizeAttributes", (t) => {
|
|
356
443
|
if (!(t instanceof Element) || !t.hasAttribute("style")) return;
|
|
357
444
|
const e = t.style, r = [];
|
|
358
445
|
for (let i = 0; i < e.length; i++) {
|
|
359
446
|
const l = e.item(i);
|
|
360
|
-
|
|
447
|
+
y.has(l) || r.push(l);
|
|
361
448
|
}
|
|
362
449
|
for (const i of r) e.removeProperty(i);
|
|
363
|
-
const
|
|
450
|
+
const n = /* @__PURE__ */ new Set([
|
|
364
451
|
"static",
|
|
365
452
|
"relative",
|
|
366
453
|
"absolute",
|
|
@@ -370,120 +457,71 @@ function E() {
|
|
|
370
457
|
"revert",
|
|
371
458
|
"revert-layer"
|
|
372
459
|
]), c = e.getPropertyValue("position").trim().toLowerCase();
|
|
373
|
-
c && !
|
|
460
|
+
c && !n.has(c) && e.removeProperty("position");
|
|
374
461
|
const d = /* @__PURE__ */ new Set(["initial", "inherit", "unset", "revert", "revert-layer"]), o = e.getPropertyValue("opacity").trim().toLowerCase();
|
|
375
462
|
o && !d.has(o) && (o.endsWith("%") ? parseFloat(o) / 100 : parseFloat(o)) < 0.05 && e.removeProperty("opacity");
|
|
376
|
-
for (const i of
|
|
463
|
+
for (const i of S) {
|
|
377
464
|
const l = e.getPropertyValue(i);
|
|
378
|
-
l &&
|
|
465
|
+
l && E.test(l) && e.removeProperty(i);
|
|
379
466
|
}
|
|
380
467
|
e.length === 0 && t.removeAttribute("style");
|
|
381
|
-
}),
|
|
382
|
-
if (!
|
|
468
|
+
}), a.addHook("uponSanitizeAttribute", (t, e) => {
|
|
469
|
+
if (!k.has(e.attrName)) return;
|
|
383
470
|
const r = e.attrValue;
|
|
384
471
|
/^\s*\/\//.test(r) ? (e.attrValue = r.trimStart().replace(/^\/\//, "https://"), e.keepAttr = !0) : /^\s*\\/.test(r) && (e.keepAttr = !1);
|
|
385
|
-
}),
|
|
472
|
+
}), a.addHook("uponSanitizeAttribute", (t, e) => {
|
|
473
|
+
if (!_.has(e.attrName)) return;
|
|
474
|
+
const r = e.attrValue;
|
|
475
|
+
if (!r.trim()) return;
|
|
476
|
+
const n = /^\s*\/\//.test(r) ? r.trimStart().replace(/^\/\//, "https://") : r;
|
|
477
|
+
e.attrValue = A(n);
|
|
478
|
+
}), a.addHook("uponSanitizeAttribute", (t, e) => {
|
|
479
|
+
const r = x[e.attrName];
|
|
480
|
+
!r || !(t instanceof Element) || (t.hasAttribute(r) || t.setAttribute(r, e.attrValue), e.keepAttr = !1);
|
|
481
|
+
}), a.addHook("afterSanitizeAttributes", (t) => {
|
|
386
482
|
if (!(t instanceof Element) || !t.hasAttribute("srcset")) return;
|
|
387
|
-
const r = (t.getAttribute("srcset") ?? "").split(","),
|
|
388
|
-
if (r.some((o) => /^\s*\\/.test(
|
|
483
|
+
const r = (t.getAttribute("srcset") ?? "").split(","), n = (o) => o.trim().split(/\s+/)[0];
|
|
484
|
+
if (r.some((o) => /^\s*\\/.test(n(o)))) {
|
|
389
485
|
t.removeAttribute("srcset");
|
|
390
486
|
return;
|
|
391
487
|
}
|
|
392
488
|
let c = !1;
|
|
393
489
|
const d = r.map((o) => {
|
|
394
|
-
const i =
|
|
490
|
+
const i = n(o);
|
|
395
491
|
if (!i.startsWith("//")) return o;
|
|
396
492
|
c = !0;
|
|
397
493
|
const l = o.indexOf(i);
|
|
398
494
|
return o.slice(0, l) + "https://" + i.slice(2) + o.slice(l + i.length);
|
|
399
495
|
});
|
|
400
496
|
c && t.setAttribute("srcset", d.join(","));
|
|
401
|
-
}),
|
|
402
|
-
var
|
|
403
|
-
if (!(t instanceof Element) || t.tagName !== "A" && t.tagName !== "AREA" || ((
|
|
497
|
+
}), a.addHook("afterSanitizeAttributes", (t) => {
|
|
498
|
+
var n;
|
|
499
|
+
if (!(t instanceof Element) || t.tagName !== "A" && t.tagName !== "AREA" || ((n = t.getAttribute("target")) == null ? void 0 : n.toLowerCase()) !== "_blank") return;
|
|
404
500
|
const e = t.getAttribute("rel") ?? "", r = new Set(e.split(/\s+/).filter(Boolean));
|
|
405
501
|
r.add("noopener"), t.setAttribute("rel", [...r].join(" "));
|
|
406
|
-
}),
|
|
502
|
+
}), a.addHook("afterSanitizeAttributes", (t) => {
|
|
407
503
|
if (!(t instanceof Element) || t.tagName !== "IFRAME" || !t.hasAttribute("sandbox")) return;
|
|
408
|
-
const r = (t.getAttribute("sandbox") ?? "").toLowerCase().split(/\s+/).filter(Boolean),
|
|
409
|
-
|
|
410
|
-
}),
|
|
504
|
+
const r = (t.getAttribute("sandbox") ?? "").toLowerCase().split(/\s+/).filter(Boolean), n = r.filter((c) => v.has(c));
|
|
505
|
+
n.length !== r.length && t.setAttribute("sandbox", n.join(" "));
|
|
506
|
+
}), a.addHook("afterSanitizeAttributes", (t) => {
|
|
411
507
|
if (!(t instanceof Element) || t.tagName !== "PARAM") return;
|
|
412
508
|
const e = t.getAttribute("value");
|
|
413
509
|
e && /^\s*(?:javascript|vbscript):/i.test(e) && t.removeAttribute("value");
|
|
414
510
|
});
|
|
415
|
-
const
|
|
416
|
-
return
|
|
511
|
+
const s = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
|
|
512
|
+
return a.addHook("afterSanitizeElements", (t) => {
|
|
417
513
|
if (!(t instanceof Element) || t.tagName.toLowerCase() !== "annotation-xml") return;
|
|
418
514
|
const e = (t.getAttribute("encoding") ?? "").toLowerCase().trim();
|
|
419
|
-
|
|
420
|
-
}),
|
|
515
|
+
s.has(e) && t.remove();
|
|
516
|
+
}), a;
|
|
421
517
|
}
|
|
422
|
-
function
|
|
518
|
+
function N(s, t) {
|
|
423
519
|
if (typeof window > "u")
|
|
424
520
|
throw new Error("sanitizeHtml requires a DOM environment (window is not defined)");
|
|
425
|
-
const e = t != null && t.allowFormAttributeNames ? { ...
|
|
426
|
-
return
|
|
427
|
-
}
|
|
428
|
-
const m = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), u = "http://platform-sanitize.invalid/", b = /^\s*\/\//, g = (
|
|
429
|
-
// oxlint-disable-next-line no-control-regex -- intentional security guard
|
|
430
|
-
/^[\u0000-\u0020\u007F-\u00A0\u2000-\u200F\u2028\u2029\u202F\u205F\u2060\u3000\uFEFF]*(?:javascript|data|vbscript|file):/i
|
|
431
|
-
), S = {
|
|
432
|
-
colon: ":",
|
|
433
|
-
// javascript:alert(1) → javascript:alert(1)
|
|
434
|
-
sol: "/",
|
|
435
|
-
// //evil.com → //evil.com (protocol-relative)
|
|
436
|
-
bsol: "\\",
|
|
437
|
-
// \\evil.com → \\evil.com
|
|
438
|
-
Tab: " ",
|
|
439
|
-
// java	script: — WHATWG strips mid-scheme tabs
|
|
440
|
-
NewLine: `
|
|
441
|
-
`
|
|
442
|
-
// java
script:
|
|
443
|
-
};
|
|
444
|
-
function x(a) {
|
|
445
|
-
return a.replace(/&#(\d+);/g, (t, e) => {
|
|
446
|
-
const r = Number(e);
|
|
447
|
-
if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
|
|
448
|
-
try {
|
|
449
|
-
return String.fromCodePoint(r);
|
|
450
|
-
} catch {
|
|
451
|
-
return "";
|
|
452
|
-
}
|
|
453
|
-
}).replace(/&#x([\da-f]+);/gi, (t, e) => {
|
|
454
|
-
const r = parseInt(e, 16);
|
|
455
|
-
if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
|
|
456
|
-
try {
|
|
457
|
-
return String.fromCodePoint(r);
|
|
458
|
-
} catch {
|
|
459
|
-
return "";
|
|
460
|
-
}
|
|
461
|
-
}).replace(/&([A-Za-z][A-Za-z0-9]*);/g, (t, e) => S[e] ?? t);
|
|
462
|
-
}
|
|
463
|
-
function _(a) {
|
|
464
|
-
if (!a || !a.trim()) return "about:blank";
|
|
465
|
-
const t = a.replace(/\\/g, "/");
|
|
466
|
-
if (b.test(t) || g.test(t)) return "about:blank";
|
|
467
|
-
if (/&[#A-Za-z]/.test(t)) {
|
|
468
|
-
const e = x(t);
|
|
469
|
-
if (b.test(e) || g.test(e))
|
|
470
|
-
return "about:blank";
|
|
471
|
-
try {
|
|
472
|
-
const r = new URL(e, u);
|
|
473
|
-
if (!r.href.startsWith(u) && !m.has(r.protocol))
|
|
474
|
-
return "about:blank";
|
|
475
|
-
} catch {
|
|
476
|
-
return "about:blank";
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
try {
|
|
480
|
-
const e = new URL(t, u);
|
|
481
|
-
return !m.has(e.protocol) || (e.protocol === "http:" || e.protocol === "https:") && (e.username || e.password) ? "about:blank" : e.href.startsWith(u) ? a : t.replace(/[\x00-\x1F\u2028\u2029]/g, "").replace(/%250[9ad]/gi, "").replace(/%0[9ad]/gi, "");
|
|
482
|
-
} catch {
|
|
483
|
-
return "about:blank";
|
|
484
|
-
}
|
|
521
|
+
const e = t != null && t.allowFormAttributeNames ? { ...g, SANITIZE_DOM: !1 } : g;
|
|
522
|
+
return R().sanitize(s ?? "", e);
|
|
485
523
|
}
|
|
486
524
|
export {
|
|
487
|
-
|
|
488
|
-
|
|
525
|
+
N as sanitizeHtml,
|
|
526
|
+
A as sanitizeUrl
|
|
489
527
|
};
|
|
@@ -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":"AAymBA,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,OAAO,CAAC,EAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9C,MAAM,CASR"}
|