@instructure/platform-sanitize 0.3.15 → 0.4.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,4 +1,4 @@
1
- import u from "dompurify";
1
+ import d from "dompurify";
2
2
  const m = /* @__PURE__ */ new Set([
3
3
  // layout
4
4
  "display",
@@ -8,6 +8,7 @@ const m = /* @__PURE__ */ new Set([
8
8
  "overflow-x",
9
9
  "overflow-y",
10
10
  "visibility",
11
+ "opacity",
11
12
  "cursor",
12
13
  "direction",
13
14
  "user-select",
@@ -205,6 +206,10 @@ const m = /* @__PURE__ */ new Set([
205
206
  "animation-direction",
206
207
  "animation-fill-mode",
207
208
  "animation-play-state",
209
+ // transforms — no value filter needed; .user_content has overflow-y:hidden and overflow-x:auto
210
+ // (canvas-lms g/412928), which together contain transformed content within the container on
211
+ // both axes and prevent overlay of Canvas UI.
212
+ "transform",
208
213
  // columns
209
214
  "column-count",
210
215
  "column-width",
@@ -247,16 +252,7 @@ const m = /* @__PURE__ */ new Set([
247
252
  "cite",
248
253
  "longdesc",
249
254
  "xlink:href"
250
- ]), h = [
251
- "background",
252
- "background-image",
253
- "list-style",
254
- "list-style-image",
255
- "cursor",
256
- // content: url(...) triggers an HTTP GET even on non-pseudo elements in some
257
- // browsers; strip it as defense-in-depth against tracking-pixel exfiltration.
258
- "content"
259
- ], y = /url\s*\(\s*['"]?(?:[a-z][a-z0-9+\-.]*:|\/\/)/i, k = /* @__PURE__ */ new Set([
255
+ ]), h = ["content"], y = /url\s*\(\s*['"]?(?:[a-z][a-z0-9+\-.]*:|\/\/)/i, A = /* @__PURE__ */ new Set([
260
256
  "allow-downloads",
261
257
  "allow-forms",
262
258
  "allow-modals",
@@ -285,13 +281,15 @@ const m = /* @__PURE__ */ new Set([
285
281
  "webkitallowfullscreen",
286
282
  "mozallowfullscreen",
287
283
  "scrolling",
284
+ // loading="lazy" is not in DOMPurify's default ALLOWED_ATTR and must be
285
+ // explicitly added. No security concern — it is a performance hint only.
286
+ "loading",
288
287
  // MathML 4 (W3C) annotation attributes for screen-reader accessibility.
289
288
  // MathCAT, JAWS, and NVDA use `intent` to know how to pronounce
290
289
  // expressions; `arg` labels sub-expressions referenced by intent.
291
290
  // These are plain string annotations — no URL-loading, no code execution.
292
291
  "intent",
293
- "arg",
294
- "loading"
292
+ "arg"
295
293
  ],
296
294
  // Rails UJS turns data-method/data-remote/etc. on clickable elements into
297
295
  // state-changing requests carrying the victim's CSRF token. Strip them so
@@ -313,15 +311,15 @@ const m = /* @__PURE__ */ new Set([
313
311
  FORCE_BODY: !0
314
312
  };
315
313
  let a = null;
316
- function A() {
317
- return a || (a = typeof u == "function" ? u(window) : u, a.addHook("afterSanitizeAttributes", (e) => {
318
- if (!(e instanceof Element) || !e.hasAttribute("style")) return;
319
- const r = e.style, t = [];
320
- for (let n = 0; n < r.length; n++) {
321
- const i = r.item(n);
322
- m.has(i) || t.push(i);
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);
323
321
  }
324
- for (const n of t) r.removeProperty(n);
322
+ for (const n of e) t.removeProperty(n);
325
323
  const o = /* @__PURE__ */ new Set([
326
324
  "static",
327
325
  "relative",
@@ -331,54 +329,56 @@ function A() {
331
329
  "unset",
332
330
  "revert",
333
331
  "revert-layer"
334
- ]), s = r.getPropertyValue("position").trim().toLowerCase();
335
- s && !o.has(s) && r.removeProperty("position");
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
336
  for (const n of h) {
337
- const i = r.getPropertyValue(n);
338
- i && y.test(i) && r.removeProperty(n);
337
+ const s = t.getPropertyValue(n);
338
+ s && y.test(s) && t.removeProperty(n);
339
339
  }
340
- r.length === 0 && e.removeAttribute("style");
341
- }), a.addHook("uponSanitizeAttribute", (e, r) => {
342
- if (!w.has(r.attrName)) return;
343
- const t = r.attrValue;
344
- /^\s*\/\//.test(t) ? (r.attrValue = t.trimStart().replace(/^\/\//, "https://"), r.keepAttr = !0) : /^\s*\\/.test(t) && (r.keepAttr = !1);
345
- }), a.addHook("afterSanitizeAttributes", (e) => {
346
- if (!(e instanceof Element) || !e.hasAttribute("srcset")) return;
347
- const t = (e.getAttribute("srcset") ?? "").split(","), o = (i) => i.trim().split(/\s+/)[0];
348
- if (t.some((i) => /^\s*\\/.test(o(i)))) {
349
- e.removeAttribute("srcset");
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");
350
350
  return;
351
351
  }
352
- let s = !1;
353
- const n = t.map((i) => {
354
- const l = o(i);
355
- if (!l.startsWith("//")) return i;
356
- s = !0;
357
- const d = i.indexOf(l);
358
- return i.slice(0, d) + "https://" + l.slice(2) + i.slice(d + l.length);
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);
359
359
  });
360
- s && e.setAttribute("srcset", n.join(","));
361
- }), a.addHook("afterSanitizeAttributes", (e) => {
360
+ l && r.setAttribute("srcset", u.join(","));
361
+ }), a.addHook("afterSanitizeAttributes", (r) => {
362
362
  var o;
363
- if (!(e instanceof Element) || e.tagName !== "A" && e.tagName !== "AREA" || ((o = e.getAttribute("target")) == null ? void 0 : o.toLowerCase()) !== "_blank") return;
364
- const r = e.getAttribute("rel") ?? "", t = new Set(r.split(/\s+/).filter(Boolean));
365
- t.add("noopener"), e.setAttribute("rel", [...t].join(" "));
366
- }), a.addHook("afterSanitizeAttributes", (e) => {
367
- if (!(e instanceof Element) || e.tagName !== "IFRAME" || !e.hasAttribute("sandbox")) return;
368
- const t = (e.getAttribute("sandbox") ?? "").toLowerCase().split(/\s+/).filter(Boolean), o = t.filter((s) => k.has(s));
369
- o.length !== t.length && e.setAttribute("sandbox", o.join(" "));
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
370
  }), a);
371
371
  }
372
- function v(e, r) {
372
+ function v(r, t) {
373
373
  if (typeof window > "u")
374
374
  throw new Error("sanitizeHtml requires a DOM environment (window is not defined)");
375
- const t = r != null && r.allowFormAttributeNames ? { ...f, SANITIZE_DOM: !1 } : f;
376
- return A().sanitize(e ?? "", t);
375
+ const e = t != null && t.allowFormAttributeNames ? { ...f, SANITIZE_DOM: !1 } : f;
376
+ return k().sanitize(r ?? "", e);
377
377
  }
378
- const p = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), c = "http://platform-sanitize.invalid/", g = /^\s*\/\//, b = (
378
+ const p = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), c = "http://platform-sanitize.invalid/", b = /^\s*\/\//, g = (
379
379
  // oxlint-disable-next-line no-control-regex -- intentional security guard
380
380
  /^[\u0000-\u0020\u007F-\u00A0\u2000-\u200F\u2028\u2029\u202F\u205F\u2060\u3000\uFEFF]*(?:javascript|data|vbscript|file):/i
381
- ), x = {
381
+ ), S = {
382
382
  colon: ":",
383
383
  // javascript&colon;alert(1) → javascript:alert(1)
384
384
  sol: "/",
@@ -391,35 +391,35 @@ const p = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), c = "
391
391
  `
392
392
  // java&NewLine;script:
393
393
  };
394
- function E(e) {
395
- return e.replace(/&#(\d+);/g, (r, t) => {
396
- const o = Number(t);
394
+ function E(r) {
395
+ return r.replace(/&#(\d+);/g, (t, e) => {
396
+ const o = Number(e);
397
397
  if (!Number.isFinite(o) || o < 0 || o > 1114111) return "";
398
398
  try {
399
399
  return String.fromCodePoint(o);
400
400
  } catch {
401
401
  return "";
402
402
  }
403
- }).replace(/&#x([\da-f]+);/gi, (r, t) => {
404
- const o = parseInt(t, 16);
403
+ }).replace(/&#x([\da-f]+);/gi, (t, e) => {
404
+ const o = parseInt(e, 16);
405
405
  if (!Number.isFinite(o) || o < 0 || o > 1114111) return "";
406
406
  try {
407
407
  return String.fromCodePoint(o);
408
408
  } catch {
409
409
  return "";
410
410
  }
411
- }).replace(/&([A-Za-z][A-Za-z0-9]*);/g, (r, t) => x[t] ?? r);
411
+ }).replace(/&([A-Za-z][A-Za-z0-9]*);/g, (t, e) => S[e] ?? t);
412
412
  }
413
- function R(e) {
414
- if (!e || !e.trim()) return "about:blank";
415
- const r = e.replace(/\\/g, "/");
416
- if (g.test(r) || b.test(r)) return "about:blank";
417
- if (/&[#A-Za-z]/.test(r)) {
418
- const t = E(r);
419
- if (g.test(t) || b.test(t))
413
+ function R(r) {
414
+ if (!r || !r.trim()) return "about:blank";
415
+ const t = r.replace(/\\/g, "/");
416
+ if (b.test(t) || g.test(t)) return "about:blank";
417
+ if (/&[#A-Za-z]/.test(t)) {
418
+ const e = E(t);
419
+ if (b.test(e) || g.test(e))
420
420
  return "about:blank";
421
421
  try {
422
- const o = new URL(t, c);
422
+ const o = new URL(e, c);
423
423
  if (!o.href.startsWith(c) && !p.has(o.protocol))
424
424
  return "about:blank";
425
425
  } catch {
@@ -427,8 +427,8 @@ function R(e) {
427
427
  }
428
428
  }
429
429
  try {
430
- const t = new URL(r, c);
431
- return !p.has(t.protocol) || (t.protocol === "http:" || t.protocol === "https:") && (t.username || t.password) ? "about:blank" : t.href.startsWith(c) ? e : r.replace(/[\x00-\x1F\u2028\u2029]/g, "").replace(/%250[9ad]/gi, "").replace(/%0[9ad]/gi, "");
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, "");
432
432
  } catch {
433
433
  return "about:blank";
434
434
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../src/sanitizeHtml.ts"],"names":[],"mappings":"AA6cA,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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/platform-sanitize",
3
- "version": "0.3.15",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",