@instructure/platform-sanitize 0.4.0 → 0.5.0

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,5 @@
1
- import d from "dompurify";
2
- const m = /* @__PURE__ */ new Set([
1
+ import f from "dompurify";
2
+ const w = /* @__PURE__ */ new Set([
3
3
  // layout
4
4
  "display",
5
5
  "float",
@@ -241,7 +241,7 @@ const m = /* @__PURE__ */ new Set([
241
241
  "caret-color",
242
242
  "accent-color",
243
243
  "appearance"
244
- ]), w = /* @__PURE__ */ new Set([
244
+ ]), h = /* @__PURE__ */ new Set([
245
245
  "src",
246
246
  "href",
247
247
  "action",
@@ -252,7 +252,7 @@ const m = /* @__PURE__ */ new Set([
252
252
  "cite",
253
253
  "longdesc",
254
254
  "xlink:href"
255
- ]), h = ["content"], y = /url\s*\(\s*['"]?(?:[a-z][a-z0-9+\-.]*:|\/\/)/i, A = /* @__PURE__ */ new Set([
255
+ ]), y = ["content"], A = /url\s*\(\s*['"]?(?:[a-z][a-z0-9+\-.]*:|\/\/)/i, k = /* @__PURE__ */ new Set([
256
256
  "allow-downloads",
257
257
  "allow-forms",
258
258
  "allow-modals",
@@ -265,8 +265,41 @@ const m = /* @__PURE__ */ new Set([
265
265
  "allow-scripts",
266
266
  "allow-storage-access-by-user-activation",
267
267
  "allow-top-navigation-by-user-activation"
268
- ]), f = {
269
- ADD_TAGS: ["iframe"],
268
+ ]), p = {
269
+ // semantics and annotation are re-allowed because DOMPurify blocks all of
270
+ // mathMlDisallowed by default. semantics is a safe structural wrapper;
271
+ // annotation carries LaTeX source text (e.g. \frac{1}{2}); Canvas emits
272
+ // text-only content here, and any child elements would be hoisted out of
273
+ // the MathML context and sanitized as ordinary HTML.
274
+ //
275
+ // annotation-xml is re-added here and filtered by an afterSanitizeElements
276
+ // hook below: it is stripped when its encoding makes it an HTML integration
277
+ // point ("text/html" or "application/xhtml+xml" per the WHATWG spec), which
278
+ // is the mXSS namespace-confusion vector. Semantic encodings used by external
279
+ // tools (MathML-Content, MathML-Presentation from MathType/Wolfram Alpha,
280
+ // application/x-tex, …) are not integration points and are preserved, so this
281
+ // layer no longer strips the benign annotation-xml the backend and TinyMCE
282
+ // already allow.
283
+ // object/embed/param re-enable PDF/SVG/media embedding in course content
284
+ // (e.g. <object data="file.pdf" type="application/pdf">). Their Flash/ActiveX
285
+ // attributes (classid, codebase, pluginspage, wmode, allowscriptaccess) are
286
+ // intentionally NOT added — dead tech, and ADD_ATTR is global in DOMPurify so
287
+ // re-allowing them would also reopen that surface on iframe. DOMPurify's URI
288
+ // filter blocks javascript:/data: on data/src, matching Canvas's
289
+ // http/https/relative protocol rule for these tags.
290
+ //
291
+ // SECURITY ASSUMPTION (same one iframe already relies on): object/embed load
292
+ // their resource as a *document*, not an image — a same-origin SVG/HTML file
293
+ // (e.g. <object data="/files/123/download" type="image/svg+xml">) executes any
294
+ // <script> it contains in the embedding page's origin. The javascript:/data:
295
+ // filter does not help here because the URL is a benign same-origin path; the
296
+ // payload lives in the fetched file. This is NOT new attack surface: <iframe>
297
+ // is already allowed and has the identical capability (its srcdoc is stripped —
298
+ // see test). The defense for all three is that Canvas serves user-uploaded
299
+ // files from a separate sandboxed origin. object/embed also have no usable
300
+ // sandbox attribute, so the sandbox-token hook cannot constrain them — again,
301
+ // parity with an unsandboxed iframe, which is already permitted.
302
+ ADD_TAGS: ["iframe", "semantics", "annotation", "annotation-xml", "object", "embed", "param"],
270
303
  ADD_ATTR: [
271
304
  "allowfullscreen",
272
305
  "allow",
@@ -274,6 +307,12 @@ const m = /* @__PURE__ */ new Set([
274
307
  "sandbox",
275
308
  "data-media-id",
276
309
  "data-media-type",
310
+ // <object>'s resource URL attribute — not in DOMPurify's defaults. src/type/
311
+ // name/value/width/height (used by object/embed/param) already are. Added
312
+ // globally (DOMPurify has no per-tag attr scoping), which is harmless: `data`
313
+ // is in URL_ATTRS so it is protocol-filtered, and it is not a DOM-clobbering
314
+ // or URI-safe attribute name.
315
+ "data",
277
316
  // RCE produces target="_blank" for external links. Modern browsers treat
278
317
  // target="_blank" as implicit rel="noopener", and the backend allowlist
279
318
  // already includes target.
@@ -310,17 +349,18 @@ const m = /* @__PURE__ */ new Set([
310
349
  // confusion attacks where fragments like <svg> could influence parse context.
311
350
  FORCE_BODY: !0
312
351
  };
313
- let a = null;
314
- function k() {
315
- return a || (a = typeof d == "function" ? d(window) : d, a.addHook("afterSanitizeAttributes", (r) => {
316
- if (!(r instanceof Element) || !r.hasAttribute("style")) return;
317
- const t = r.style, e = [];
318
- for (let n = 0; n < t.length; n++) {
319
- const s = t.item(n);
320
- m.has(s) || e.push(s);
352
+ let n = null;
353
+ function E() {
354
+ if (n) return n;
355
+ n = typeof f == "function" ? f(window) : f, n.addHook("afterSanitizeAttributes", (t) => {
356
+ if (!(t instanceof Element) || !t.hasAttribute("style")) return;
357
+ const e = t.style, r = [];
358
+ for (let i = 0; i < e.length; i++) {
359
+ const l = e.item(i);
360
+ w.has(l) || r.push(l);
321
361
  }
322
- for (const n of e) t.removeProperty(n);
323
- const o = /* @__PURE__ */ new Set([
362
+ for (const i of r) e.removeProperty(i);
363
+ const s = /* @__PURE__ */ new Set([
324
364
  "static",
325
365
  "relative",
326
366
  "absolute",
@@ -329,53 +369,63 @@ function k() {
329
369
  "unset",
330
370
  "revert",
331
371
  "revert-layer"
332
- ]), l = t.getPropertyValue("position").trim().toLowerCase();
333
- l && !o.has(l) && t.removeProperty("position");
334
- const u = /* @__PURE__ */ new Set(["initial", "inherit", "unset", "revert", "revert-layer"]), i = t.getPropertyValue("opacity").trim().toLowerCase();
335
- i && !u.has(i) && (i.endsWith("%") ? parseFloat(i) / 100 : parseFloat(i)) < 0.05 && t.removeProperty("opacity");
336
- for (const n of h) {
337
- const s = t.getPropertyValue(n);
338
- s && y.test(s) && t.removeProperty(n);
372
+ ]), c = e.getPropertyValue("position").trim().toLowerCase();
373
+ c && !s.has(c) && e.removeProperty("position");
374
+ const d = /* @__PURE__ */ new Set(["initial", "inherit", "unset", "revert", "revert-layer"]), o = e.getPropertyValue("opacity").trim().toLowerCase();
375
+ o && !d.has(o) && (o.endsWith("%") ? parseFloat(o) / 100 : parseFloat(o)) < 0.05 && e.removeProperty("opacity");
376
+ for (const i of y) {
377
+ const l = e.getPropertyValue(i);
378
+ l && A.test(l) && e.removeProperty(i);
339
379
  }
340
- t.length === 0 && r.removeAttribute("style");
341
- }), a.addHook("uponSanitizeAttribute", (r, t) => {
342
- if (!w.has(t.attrName)) return;
343
- const e = t.attrValue;
344
- /^\s*\/\//.test(e) ? (t.attrValue = e.trimStart().replace(/^\/\//, "https://"), t.keepAttr = !0) : /^\s*\\/.test(e) && (t.keepAttr = !1);
345
- }), a.addHook("afterSanitizeAttributes", (r) => {
346
- if (!(r instanceof Element) || !r.hasAttribute("srcset")) return;
347
- const e = (r.getAttribute("srcset") ?? "").split(","), o = (i) => i.trim().split(/\s+/)[0];
348
- if (e.some((i) => /^\s*\\/.test(o(i)))) {
349
- r.removeAttribute("srcset");
380
+ e.length === 0 && t.removeAttribute("style");
381
+ }), n.addHook("uponSanitizeAttribute", (t, e) => {
382
+ if (!h.has(e.attrName)) return;
383
+ const r = e.attrValue;
384
+ /^\s*\/\//.test(r) ? (e.attrValue = r.trimStart().replace(/^\/\//, "https://"), e.keepAttr = !0) : /^\s*\\/.test(r) && (e.keepAttr = !1);
385
+ }), n.addHook("afterSanitizeAttributes", (t) => {
386
+ 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)))) {
389
+ t.removeAttribute("srcset");
350
390
  return;
351
391
  }
352
- let l = !1;
353
- const u = e.map((i) => {
354
- const n = o(i);
355
- if (!n.startsWith("//")) return i;
356
- l = !0;
357
- const s = i.indexOf(n);
358
- return i.slice(0, s) + "https://" + n.slice(2) + i.slice(s + n.length);
392
+ let c = !1;
393
+ const d = r.map((o) => {
394
+ const i = s(o);
395
+ if (!i.startsWith("//")) return o;
396
+ c = !0;
397
+ const l = o.indexOf(i);
398
+ return o.slice(0, l) + "https://" + i.slice(2) + o.slice(l + i.length);
359
399
  });
360
- l && r.setAttribute("srcset", u.join(","));
361
- }), a.addHook("afterSanitizeAttributes", (r) => {
362
- var o;
363
- if (!(r instanceof Element) || r.tagName !== "A" && r.tagName !== "AREA" || ((o = r.getAttribute("target")) == null ? void 0 : o.toLowerCase()) !== "_blank") return;
364
- const t = r.getAttribute("rel") ?? "", e = new Set(t.split(/\s+/).filter(Boolean));
365
- e.add("noopener"), r.setAttribute("rel", [...e].join(" "));
366
- }), a.addHook("afterSanitizeAttributes", (r) => {
367
- if (!(r instanceof Element) || r.tagName !== "IFRAME" || !r.hasAttribute("sandbox")) return;
368
- const e = (r.getAttribute("sandbox") ?? "").toLowerCase().split(/\s+/).filter(Boolean), o = e.filter((l) => A.has(l));
369
- o.length !== e.length && r.setAttribute("sandbox", o.join(" "));
370
- }), a);
400
+ 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;
404
+ const e = t.getAttribute("rel") ?? "", r = new Set(e.split(/\s+/).filter(Boolean));
405
+ r.add("noopener"), t.setAttribute("rel", [...r].join(" "));
406
+ }), n.addHook("afterSanitizeAttributes", (t) => {
407
+ 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) => {
411
+ if (!(t instanceof Element) || t.tagName !== "PARAM") return;
412
+ const e = t.getAttribute("value");
413
+ e && /^\s*(?:javascript|vbscript):/i.test(e) && t.removeAttribute("value");
414
+ });
415
+ const a = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
416
+ return n.addHook("afterSanitizeElements", (t) => {
417
+ if (!(t instanceof Element) || t.tagName.toLowerCase() !== "annotation-xml") return;
418
+ const e = (t.getAttribute("encoding") ?? "").toLowerCase().trim();
419
+ a.has(e) && t.remove();
420
+ }), n;
371
421
  }
372
- function v(r, t) {
422
+ function R(a, t) {
373
423
  if (typeof window > "u")
374
424
  throw new Error("sanitizeHtml requires a DOM environment (window is not defined)");
375
- const e = t != null && t.allowFormAttributeNames ? { ...f, SANITIZE_DOM: !1 } : f;
376
- return k().sanitize(r ?? "", e);
425
+ const e = t != null && t.allowFormAttributeNames ? { ...p, SANITIZE_DOM: !1 } : p;
426
+ return E().sanitize(a ?? "", e);
377
427
  }
378
- const p = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), c = "http://platform-sanitize.invalid/", b = /^\s*\/\//, g = (
428
+ const m = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), u = "http://platform-sanitize.invalid/", b = /^\s*\/\//, g = (
379
429
  // oxlint-disable-next-line no-control-regex -- intentional security guard
380
430
  /^[\u0000-\u0020\u007F-\u00A0\u2000-\u200F\u2028\u2029\u202F\u205F\u2060\u3000\uFEFF]*(?:javascript|data|vbscript|file):/i
381
431
  ), S = {
@@ -391,49 +441,49 @@ const p = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), c = "
391
441
  `
392
442
  // java&NewLine;script:
393
443
  };
394
- function E(r) {
395
- return r.replace(/&#(\d+);/g, (t, e) => {
396
- const o = Number(e);
397
- if (!Number.isFinite(o) || o < 0 || o > 1114111) return "";
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 "";
398
448
  try {
399
- return String.fromCodePoint(o);
449
+ return String.fromCodePoint(r);
400
450
  } catch {
401
451
  return "";
402
452
  }
403
453
  }).replace(/&#x([\da-f]+);/gi, (t, e) => {
404
- const o = parseInt(e, 16);
405
- if (!Number.isFinite(o) || o < 0 || o > 1114111) return "";
454
+ const r = parseInt(e, 16);
455
+ if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
406
456
  try {
407
- return String.fromCodePoint(o);
457
+ return String.fromCodePoint(r);
408
458
  } catch {
409
459
  return "";
410
460
  }
411
461
  }).replace(/&([A-Za-z][A-Za-z0-9]*);/g, (t, e) => S[e] ?? t);
412
462
  }
413
- function R(r) {
414
- if (!r || !r.trim()) return "about:blank";
415
- const t = r.replace(/\\/g, "/");
463
+ function _(a) {
464
+ if (!a || !a.trim()) return "about:blank";
465
+ const t = a.replace(/\\/g, "/");
416
466
  if (b.test(t) || g.test(t)) return "about:blank";
417
467
  if (/&[#A-Za-z]/.test(t)) {
418
- const e = E(t);
468
+ const e = x(t);
419
469
  if (b.test(e) || g.test(e))
420
470
  return "about:blank";
421
471
  try {
422
- const o = new URL(e, c);
423
- if (!o.href.startsWith(c) && !p.has(o.protocol))
472
+ const r = new URL(e, u);
473
+ if (!r.href.startsWith(u) && !m.has(r.protocol))
424
474
  return "about:blank";
425
475
  } catch {
426
476
  return "about:blank";
427
477
  }
428
478
  }
429
479
  try {
430
- const e = new URL(t, c);
431
- return !p.has(e.protocol) || (e.protocol === "http:" || e.protocol === "https:") && (e.username || e.password) ? "about:blank" : e.href.startsWith(c) ? r : t.replace(/[\x00-\x1F\u2028\u2029]/g, "").replace(/%250[9ad]/gi, "").replace(/%0[9ad]/gi, "");
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, "");
432
482
  } catch {
433
483
  return "about:blank";
434
484
  }
435
485
  }
436
486
  export {
437
- v as sanitizeHtml,
438
- R as sanitizeUrl
487
+ R as sanitizeHtml,
488
+ _ as sanitizeUrl
439
489
  };
@@ -1 +1 @@
1
- {"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../src/sanitizeHtml.ts"],"names":[],"mappings":"AA6dA,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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/platform-sanitize",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",