@instructure/platform-sanitize 0.5.0 → 0.5.1

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
1
  import f from "dompurify";
2
- const w = /* @__PURE__ */ new Set([
2
+ const m = /* @__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) && !m.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 !m.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
+ ]), S = /* @__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
+ ]), k = ["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
@@ -330,10 +395,6 @@ const w = /* @__PURE__ */ new Set([
330
395
  "intent",
331
396
  "arg"
332
397
  ],
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
398
  // In browsers that support the Trusted Types API (Chromium, Firefox),
338
399
  // DOMPurify returns a TrustedHTML object rather than a plain string. The
339
400
  // exported sanitizeHtml signature declares string — a deliberate lie that
@@ -349,18 +410,18 @@ const w = /* @__PURE__ */ new Set([
349
410
  // confusion attacks where fragments like <svg> could influence parse context.
350
411
  FORCE_BODY: !0
351
412
  };
352
- let n = null;
353
- function E() {
354
- if (n) return n;
355
- n = typeof f == "function" ? f(window) : f, n.addHook("afterSanitizeAttributes", (t) => {
413
+ let a = null;
414
+ function R() {
415
+ if (a) return a;
416
+ a = typeof f == "function" ? f(window) : f, a.addHook("afterSanitizeAttributes", (t) => {
356
417
  if (!(t instanceof Element) || !t.hasAttribute("style")) return;
357
418
  const e = t.style, r = [];
358
419
  for (let i = 0; i < e.length; i++) {
359
420
  const l = e.item(i);
360
- w.has(l) || r.push(l);
421
+ y.has(l) || r.push(l);
361
422
  }
362
423
  for (const i of r) e.removeProperty(i);
363
- const s = /* @__PURE__ */ new Set([
424
+ const n = /* @__PURE__ */ new Set([
364
425
  "static",
365
426
  "relative",
366
427
  "absolute",
@@ -370,120 +431,71 @@ function E() {
370
431
  "revert",
371
432
  "revert-layer"
372
433
  ]), c = e.getPropertyValue("position").trim().toLowerCase();
373
- c && !s.has(c) && e.removeProperty("position");
434
+ c && !n.has(c) && e.removeProperty("position");
374
435
  const d = /* @__PURE__ */ new Set(["initial", "inherit", "unset", "revert", "revert-layer"]), o = e.getPropertyValue("opacity").trim().toLowerCase();
375
436
  o && !d.has(o) && (o.endsWith("%") ? parseFloat(o) / 100 : parseFloat(o)) < 0.05 && e.removeProperty("opacity");
376
- for (const i of y) {
437
+ for (const i of k) {
377
438
  const l = e.getPropertyValue(i);
378
- l && A.test(l) && e.removeProperty(i);
439
+ l && E.test(l) && e.removeProperty(i);
379
440
  }
380
441
  e.length === 0 && t.removeAttribute("style");
381
- }), n.addHook("uponSanitizeAttribute", (t, e) => {
382
- if (!h.has(e.attrName)) return;
442
+ }), a.addHook("uponSanitizeAttribute", (t, e) => {
443
+ if (!S.has(e.attrName)) return;
383
444
  const r = e.attrValue;
384
445
  /^\s*\/\//.test(r) ? (e.attrValue = r.trimStart().replace(/^\/\//, "https://"), e.keepAttr = !0) : /^\s*\\/.test(r) && (e.keepAttr = !1);
385
- }), n.addHook("afterSanitizeAttributes", (t) => {
446
+ }), a.addHook("uponSanitizeAttribute", (t, e) => {
447
+ if (!_.has(e.attrName)) return;
448
+ const r = e.attrValue;
449
+ if (!r.trim()) return;
450
+ const n = /^\s*\/\//.test(r) ? r.trimStart().replace(/^\/\//, "https://") : r;
451
+ e.attrValue = A(n);
452
+ }), a.addHook("uponSanitizeAttribute", (t, e) => {
453
+ const r = x[e.attrName];
454
+ !r || !(t instanceof Element) || (t.hasAttribute(r) || t.setAttribute(r, e.attrValue), e.keepAttr = !1);
455
+ }), a.addHook("afterSanitizeAttributes", (t) => {
386
456
  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)))) {
457
+ const r = (t.getAttribute("srcset") ?? "").split(","), n = (o) => o.trim().split(/\s+/)[0];
458
+ if (r.some((o) => /^\s*\\/.test(n(o)))) {
389
459
  t.removeAttribute("srcset");
390
460
  return;
391
461
  }
392
462
  let c = !1;
393
463
  const d = r.map((o) => {
394
- const i = s(o);
464
+ const i = n(o);
395
465
  if (!i.startsWith("//")) return o;
396
466
  c = !0;
397
467
  const l = o.indexOf(i);
398
468
  return o.slice(0, l) + "https://" + i.slice(2) + o.slice(l + i.length);
399
469
  });
400
470
  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;
471
+ }), a.addHook("afterSanitizeAttributes", (t) => {
472
+ var n;
473
+ if (!(t instanceof Element) || t.tagName !== "A" && t.tagName !== "AREA" || ((n = t.getAttribute("target")) == null ? void 0 : n.toLowerCase()) !== "_blank") return;
404
474
  const e = t.getAttribute("rel") ?? "", r = new Set(e.split(/\s+/).filter(Boolean));
405
475
  r.add("noopener"), t.setAttribute("rel", [...r].join(" "));
406
- }), n.addHook("afterSanitizeAttributes", (t) => {
476
+ }), a.addHook("afterSanitizeAttributes", (t) => {
407
477
  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) => {
478
+ const r = (t.getAttribute("sandbox") ?? "").toLowerCase().split(/\s+/).filter(Boolean), n = r.filter((c) => v.has(c));
479
+ n.length !== r.length && t.setAttribute("sandbox", n.join(" "));
480
+ }), a.addHook("afterSanitizeAttributes", (t) => {
411
481
  if (!(t instanceof Element) || t.tagName !== "PARAM") return;
412
482
  const e = t.getAttribute("value");
413
483
  e && /^\s*(?:javascript|vbscript):/i.test(e) && t.removeAttribute("value");
414
484
  });
415
- const a = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
416
- return n.addHook("afterSanitizeElements", (t) => {
485
+ const s = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
486
+ return a.addHook("afterSanitizeElements", (t) => {
417
487
  if (!(t instanceof Element) || t.tagName.toLowerCase() !== "annotation-xml") return;
418
488
  const e = (t.getAttribute("encoding") ?? "").toLowerCase().trim();
419
- a.has(e) && t.remove();
420
- }), n;
489
+ s.has(e) && t.remove();
490
+ }), a;
421
491
  }
422
- function R(a, t) {
492
+ function N(s, t) {
423
493
  if (typeof window > "u")
424
494
  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
- }
495
+ const e = t != null && t.allowFormAttributeNames ? { ...g, SANITIZE_DOM: !1 } : g;
496
+ return R().sanitize(s ?? "", e);
485
497
  }
486
498
  export {
487
- R as sanitizeHtml,
488
- _ as sanitizeUrl
499
+ N as sanitizeHtml,
500
+ A as sanitizeUrl
489
501
  };
@@ -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":"AA+kBA,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.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",