@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 +63 -34
- package/dist/sanitizeHtml.d.ts.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
352
|
+
let n = null;
|
|
328
353
|
function E() {
|
|
329
|
-
if (
|
|
330
|
-
|
|
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
|
|
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 && !
|
|
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
|
-
}),
|
|
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
|
-
}),
|
|
385
|
+
}), n.addHook("afterSanitizeAttributes", (t) => {
|
|
361
386
|
if (!(t instanceof Element) || !t.hasAttribute("srcset")) return;
|
|
362
|
-
const r = (t.getAttribute("srcset") ?? "").split(","),
|
|
363
|
-
if (r.some((o) => /^\s*\\/.test(
|
|
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 =
|
|
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
|
-
}),
|
|
377
|
-
var
|
|
378
|
-
if (!(t instanceof Element) || t.tagName !== "A" && t.tagName !== "AREA" || ((
|
|
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
|
-
}),
|
|
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),
|
|
384
|
-
|
|
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
|
|
387
|
-
return
|
|
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
|
-
|
|
391
|
-
}),
|
|
419
|
+
a.has(e) && t.remove();
|
|
420
|
+
}), n;
|
|
392
421
|
}
|
|
393
|
-
function
|
|
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(
|
|
426
|
+
return E().sanitize(a ?? "", e);
|
|
398
427
|
}
|
|
399
|
-
const m = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), u = "http://platform-sanitize.invalid/",
|
|
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
script:
|
|
414
443
|
};
|
|
415
|
-
function x(
|
|
416
|
-
return
|
|
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
|
|
435
|
-
if (!
|
|
436
|
-
const t =
|
|
437
|
-
if (
|
|
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 (
|
|
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) ?
|
|
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
|
-
|
|
459
|
-
|
|
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":"
|
|
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"}
|