@instructure/platform-sanitize 0.4.1 → 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
@@ -280,7 +280,26 @@ const w = /* @__PURE__ */ new Set([
280
280
  // application/x-tex, …) are not integration points and are preserved, so this
281
281
  // layer no longer strips the benign annotation-xml the backend and TinyMCE
282
282
  // already allow.
283
- ADD_TAGS: ["iframe", "semantics", "annotation", "annotation-xml"],
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"],
284
303
  ADD_ATTR: [
285
304
  "allowfullscreen",
286
305
  "allow",
@@ -288,6 +307,12 @@ const w = /* @__PURE__ */ new Set([
288
307
  "sandbox",
289
308
  "data-media-id",
290
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",
291
316
  // RCE produces target="_blank" for external links. Modern browsers treat
292
317
  // target="_blank" as implicit rel="noopener", and the backend allowlist
293
318
  // already includes target.
@@ -324,10 +349,10 @@ const w = /* @__PURE__ */ new Set([
324
349
  // confusion attacks where fragments like <svg> could influence parse context.
325
350
  FORCE_BODY: !0
326
351
  };
327
- let s = null;
352
+ let n = null;
328
353
  function E() {
329
- if (s) return s;
330
- s = typeof f == "function" ? f(window) : f, s.addHook("afterSanitizeAttributes", (t) => {
354
+ if (n) return n;
355
+ n = typeof f == "function" ? f(window) : f, n.addHook("afterSanitizeAttributes", (t) => {
331
356
  if (!(t instanceof Element) || !t.hasAttribute("style")) return;
332
357
  const e = t.style, r = [];
333
358
  for (let i = 0; i < e.length; i++) {
@@ -335,7 +360,7 @@ function E() {
335
360
  w.has(l) || r.push(l);
336
361
  }
337
362
  for (const i of r) e.removeProperty(i);
338
- const a = /* @__PURE__ */ new Set([
363
+ const s = /* @__PURE__ */ new Set([
339
364
  "static",
340
365
  "relative",
341
366
  "absolute",
@@ -345,7 +370,7 @@ function E() {
345
370
  "revert",
346
371
  "revert-layer"
347
372
  ]), c = e.getPropertyValue("position").trim().toLowerCase();
348
- c && !a.has(c) && e.removeProperty("position");
373
+ c && !s.has(c) && e.removeProperty("position");
349
374
  const d = /* @__PURE__ */ new Set(["initial", "inherit", "unset", "revert", "revert-layer"]), o = e.getPropertyValue("opacity").trim().toLowerCase();
350
375
  o && !d.has(o) && (o.endsWith("%") ? parseFloat(o) / 100 : parseFloat(o)) < 0.05 && e.removeProperty("opacity");
351
376
  for (const i of y) {
@@ -353,50 +378,54 @@ function E() {
353
378
  l && A.test(l) && e.removeProperty(i);
354
379
  }
355
380
  e.length === 0 && t.removeAttribute("style");
356
- }), s.addHook("uponSanitizeAttribute", (t, e) => {
381
+ }), n.addHook("uponSanitizeAttribute", (t, e) => {
357
382
  if (!h.has(e.attrName)) return;
358
383
  const r = e.attrValue;
359
384
  /^\s*\/\//.test(r) ? (e.attrValue = r.trimStart().replace(/^\/\//, "https://"), e.keepAttr = !0) : /^\s*\\/.test(r) && (e.keepAttr = !1);
360
- }), s.addHook("afterSanitizeAttributes", (t) => {
385
+ }), n.addHook("afterSanitizeAttributes", (t) => {
361
386
  if (!(t instanceof Element) || !t.hasAttribute("srcset")) return;
362
- const r = (t.getAttribute("srcset") ?? "").split(","), a = (o) => o.trim().split(/\s+/)[0];
363
- if (r.some((o) => /^\s*\\/.test(a(o)))) {
387
+ const r = (t.getAttribute("srcset") ?? "").split(","), s = (o) => o.trim().split(/\s+/)[0];
388
+ if (r.some((o) => /^\s*\\/.test(s(o)))) {
364
389
  t.removeAttribute("srcset");
365
390
  return;
366
391
  }
367
392
  let c = !1;
368
393
  const d = r.map((o) => {
369
- const i = a(o);
394
+ const i = s(o);
370
395
  if (!i.startsWith("//")) return o;
371
396
  c = !0;
372
397
  const l = o.indexOf(i);
373
398
  return o.slice(0, l) + "https://" + i.slice(2) + o.slice(l + i.length);
374
399
  });
375
400
  c && t.setAttribute("srcset", d.join(","));
376
- }), s.addHook("afterSanitizeAttributes", (t) => {
377
- var a;
378
- if (!(t instanceof Element) || t.tagName !== "A" && t.tagName !== "AREA" || ((a = t.getAttribute("target")) == null ? void 0 : a.toLowerCase()) !== "_blank") return;
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;
379
404
  const e = t.getAttribute("rel") ?? "", r = new Set(e.split(/\s+/).filter(Boolean));
380
405
  r.add("noopener"), t.setAttribute("rel", [...r].join(" "));
381
- }), s.addHook("afterSanitizeAttributes", (t) => {
406
+ }), n.addHook("afterSanitizeAttributes", (t) => {
382
407
  if (!(t instanceof Element) || t.tagName !== "IFRAME" || !t.hasAttribute("sandbox")) return;
383
- const r = (t.getAttribute("sandbox") ?? "").toLowerCase().split(/\s+/).filter(Boolean), a = r.filter((c) => k.has(c));
384
- a.length !== r.length && t.setAttribute("sandbox", a.join(" "));
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");
385
414
  });
386
- const n = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
387
- return s.addHook("afterSanitizeElements", (t) => {
415
+ const a = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
416
+ return n.addHook("afterSanitizeElements", (t) => {
388
417
  if (!(t instanceof Element) || t.tagName.toLowerCase() !== "annotation-xml") return;
389
418
  const e = (t.getAttribute("encoding") ?? "").toLowerCase().trim();
390
- n.has(e) && t.remove();
391
- }), s;
419
+ a.has(e) && t.remove();
420
+ }), n;
392
421
  }
393
- function _(n, t) {
422
+ function R(a, t) {
394
423
  if (typeof window > "u")
395
424
  throw new Error("sanitizeHtml requires a DOM environment (window is not defined)");
396
425
  const e = t != null && t.allowFormAttributeNames ? { ...p, SANITIZE_DOM: !1 } : p;
397
- return E().sanitize(n ?? "", e);
426
+ return E().sanitize(a ?? "", e);
398
427
  }
399
- const m = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), u = "http://platform-sanitize.invalid/", g = /^\s*\/\//, b = (
428
+ const m = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), u = "http://platform-sanitize.invalid/", b = /^\s*\/\//, g = (
400
429
  // oxlint-disable-next-line no-control-regex -- intentional security guard
401
430
  /^[\u0000-\u0020\u007F-\u00A0\u2000-\u200F\u2028\u2029\u202F\u205F\u2060\u3000\uFEFF]*(?:javascript|data|vbscript|file):/i
402
431
  ), S = {
@@ -412,8 +441,8 @@ const m = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), u = "
412
441
  `
413
442
  // java&NewLine;script:
414
443
  };
415
- function x(n) {
416
- return n.replace(/&#(\d+);/g, (t, e) => {
444
+ function x(a) {
445
+ return a.replace(/&#(\d+);/g, (t, e) => {
417
446
  const r = Number(e);
418
447
  if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
419
448
  try {
@@ -431,13 +460,13 @@ function x(n) {
431
460
  }
432
461
  }).replace(/&([A-Za-z][A-Za-z0-9]*);/g, (t, e) => S[e] ?? t);
433
462
  }
434
- function R(n) {
435
- if (!n || !n.trim()) return "about:blank";
436
- const t = n.replace(/\\/g, "/");
437
- if (g.test(t) || b.test(t)) return "about:blank";
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";
438
467
  if (/&[#A-Za-z]/.test(t)) {
439
468
  const e = x(t);
440
- if (g.test(e) || b.test(e))
469
+ if (b.test(e) || g.test(e))
441
470
  return "about:blank";
442
471
  try {
443
472
  const r = new URL(e, u);
@@ -449,12 +478,12 @@ function R(n) {
449
478
  }
450
479
  try {
451
480
  const e = new URL(t, u);
452
- return !m.has(e.protocol) || (e.protocol === "http:" || e.protocol === "https:") && (e.username || e.password) ? "about:blank" : e.href.startsWith(u) ? n : t.replace(/[\x00-\x1F\u2028\u2029]/g, "").replace(/%250[9ad]/gi, "").replace(/%0[9ad]/gi, "");
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, "");
453
482
  } catch {
454
483
  return "about:blank";
455
484
  }
456
485
  }
457
486
  export {
458
- _ as sanitizeHtml,
459
- R as sanitizeUrl
487
+ R as sanitizeHtml,
488
+ _ as sanitizeUrl
460
489
  };
@@ -1 +1 @@
1
- {"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../src/sanitizeHtml.ts"],"names":[],"mappings":"AA8fA,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.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",