@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 CHANGED
@@ -1,5 +1,63 @@
1
- import f from "dompurify";
2
- const w = /* @__PURE__ */ new Set([
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
- ]), h = /* @__PURE__ */ new Set([
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
- ]), y = ["content"], A = /url\s*\(\s*['"]?(?:[a-z][a-z0-9+\-.]*:|\/\/)/i, k = /* @__PURE__ */ new Set([
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
- ]), p = {
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
- ADD_TAGS: ["iframe", "semantics", "annotation", "annotation-xml", "object", "embed", "param"],
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 n = null;
353
- function E() {
354
- if (n) return n;
355
- n = typeof f == "function" ? f(window) : f, n.addHook("afterSanitizeAttributes", (t) => {
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
- w.has(l) || r.push(l);
447
+ y.has(l) || r.push(l);
361
448
  }
362
449
  for (const i of r) e.removeProperty(i);
363
- const s = /* @__PURE__ */ new Set([
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 && !s.has(c) && e.removeProperty("position");
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 y) {
463
+ for (const i of S) {
377
464
  const l = e.getPropertyValue(i);
378
- l && A.test(l) && e.removeProperty(i);
465
+ l && E.test(l) && e.removeProperty(i);
379
466
  }
380
467
  e.length === 0 && t.removeAttribute("style");
381
- }), n.addHook("uponSanitizeAttribute", (t, e) => {
382
- if (!h.has(e.attrName)) return;
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
- }), n.addHook("afterSanitizeAttributes", (t) => {
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(","), s = (o) => o.trim().split(/\s+/)[0];
388
- if (r.some((o) => /^\s*\\/.test(s(o)))) {
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 = s(o);
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
- }), n.addHook("afterSanitizeAttributes", (t) => {
402
- var s;
403
- if (!(t instanceof Element) || t.tagName !== "A" && t.tagName !== "AREA" || ((s = t.getAttribute("target")) == null ? void 0 : s.toLowerCase()) !== "_blank") return;
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
- }), n.addHook("afterSanitizeAttributes", (t) => {
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), s = r.filter((c) => k.has(c));
409
- s.length !== r.length && t.setAttribute("sandbox", s.join(" "));
410
- }), n.addHook("afterSanitizeAttributes", (t) => {
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 a = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
416
- return n.addHook("afterSanitizeElements", (t) => {
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
- a.has(e) && t.remove();
420
- }), n;
515
+ s.has(e) && t.remove();
516
+ }), a;
421
517
  }
422
- function R(a, t) {
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 ? { ...p, SANITIZE_DOM: !1 } : p;
426
- return E().sanitize(a ?? "", e);
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&colon;alert(1) → javascript:alert(1)
434
- sol: "/",
435
- // &sol;&sol;evil.com → //evil.com (protocol-relative)
436
- bsol: "\\",
437
- // &bsol;&bsol;evil.com → \\evil.com
438
- Tab: " ",
439
- // java&Tab;script: — WHATWG strips mid-scheme tabs
440
- NewLine: `
441
- `
442
- // java&NewLine;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
- R as sanitizeHtml,
488
- _ as sanitizeUrl
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":"AAoiBA,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"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/platform-sanitize",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",