@instructure/platform-sanitize 0.4.1 → 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 +138 -97
- package/dist/sanitizeHtml.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
import f from "dompurify";
|
|
2
|
-
const
|
|
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
|
-
]),
|
|
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
|
-
]),
|
|
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
|
-
]),
|
|
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
|
|
@@ -280,7 +345,26 @@ const w = /* @__PURE__ */ new Set([
|
|
|
280
345
|
// application/x-tex, …) are not integration points and are preserved, so this
|
|
281
346
|
// layer no longer strips the benign annotation-xml the backend and TinyMCE
|
|
282
347
|
// already allow.
|
|
283
|
-
|
|
348
|
+
// object/embed/param re-enable PDF/SVG/media embedding in course content
|
|
349
|
+
// (e.g. <object data="file.pdf" type="application/pdf">). Their Flash/ActiveX
|
|
350
|
+
// attributes (classid, codebase, pluginspage, wmode, allowscriptaccess) are
|
|
351
|
+
// intentionally NOT added — dead tech, and ADD_ATTR is global in DOMPurify so
|
|
352
|
+
// re-allowing them would also reopen that surface on iframe. DOMPurify's URI
|
|
353
|
+
// filter blocks javascript:/data: on data/src, matching Canvas's
|
|
354
|
+
// http/https/relative protocol rule for these tags.
|
|
355
|
+
//
|
|
356
|
+
// SECURITY ASSUMPTION (same one iframe already relies on): object/embed load
|
|
357
|
+
// their resource as a *document*, not an image — a same-origin SVG/HTML file
|
|
358
|
+
// (e.g. <object data="/files/123/download" type="image/svg+xml">) executes any
|
|
359
|
+
// <script> it contains in the embedding page's origin. The javascript:/data:
|
|
360
|
+
// filter does not help here because the URL is a benign same-origin path; the
|
|
361
|
+
// payload lives in the fetched file. This is NOT new attack surface: <iframe>
|
|
362
|
+
// is already allowed and has the identical capability (its srcdoc is stripped —
|
|
363
|
+
// see test). The defense for all three is that Canvas serves user-uploaded
|
|
364
|
+
// files from a separate sandboxed origin. object/embed also have no usable
|
|
365
|
+
// sandbox attribute, so the sandbox-token hook cannot constrain them — again,
|
|
366
|
+
// parity with an unsandboxed iframe, which is already permitted.
|
|
367
|
+
ADD_TAGS: ["iframe", "semantics", "annotation", "annotation-xml", "object", "embed", "param"],
|
|
284
368
|
ADD_ATTR: [
|
|
285
369
|
"allowfullscreen",
|
|
286
370
|
"allow",
|
|
@@ -288,6 +372,12 @@ const w = /* @__PURE__ */ new Set([
|
|
|
288
372
|
"sandbox",
|
|
289
373
|
"data-media-id",
|
|
290
374
|
"data-media-type",
|
|
375
|
+
// <object>'s resource URL attribute — not in DOMPurify's defaults. src/type/
|
|
376
|
+
// name/value/width/height (used by object/embed/param) already are. Added
|
|
377
|
+
// globally (DOMPurify has no per-tag attr scoping), which is harmless: `data`
|
|
378
|
+
// is in URL_ATTRS so it is protocol-filtered, and it is not a DOM-clobbering
|
|
379
|
+
// or URI-safe attribute name.
|
|
380
|
+
"data",
|
|
291
381
|
// RCE produces target="_blank" for external links. Modern browsers treat
|
|
292
382
|
// target="_blank" as implicit rel="noopener", and the backend allowlist
|
|
293
383
|
// already includes target.
|
|
@@ -305,10 +395,6 @@ const w = /* @__PURE__ */ new Set([
|
|
|
305
395
|
"intent",
|
|
306
396
|
"arg"
|
|
307
397
|
],
|
|
308
|
-
// Rails UJS turns data-method/data-remote/etc. on clickable elements into
|
|
309
|
-
// state-changing requests carrying the victim's CSRF token. Strip them so
|
|
310
|
-
// user-authored HTML cannot become a CSRF gadget when UJS is on the page.
|
|
311
|
-
FORBID_ATTR: ["data-method", "data-remote", "data-url", "data-confirm", "data-disable-with"],
|
|
312
398
|
// In browsers that support the Trusted Types API (Chromium, Firefox),
|
|
313
399
|
// DOMPurify returns a TrustedHTML object rather than a plain string. The
|
|
314
400
|
// exported sanitizeHtml signature declares string — a deliberate lie that
|
|
@@ -324,18 +410,18 @@ const w = /* @__PURE__ */ new Set([
|
|
|
324
410
|
// confusion attacks where fragments like <svg> could influence parse context.
|
|
325
411
|
FORCE_BODY: !0
|
|
326
412
|
};
|
|
327
|
-
let
|
|
328
|
-
function
|
|
329
|
-
if (
|
|
330
|
-
|
|
413
|
+
let a = null;
|
|
414
|
+
function R() {
|
|
415
|
+
if (a) return a;
|
|
416
|
+
a = typeof f == "function" ? f(window) : f, a.addHook("afterSanitizeAttributes", (t) => {
|
|
331
417
|
if (!(t instanceof Element) || !t.hasAttribute("style")) return;
|
|
332
418
|
const e = t.style, r = [];
|
|
333
419
|
for (let i = 0; i < e.length; i++) {
|
|
334
420
|
const l = e.item(i);
|
|
335
|
-
|
|
421
|
+
y.has(l) || r.push(l);
|
|
336
422
|
}
|
|
337
423
|
for (const i of r) e.removeProperty(i);
|
|
338
|
-
const
|
|
424
|
+
const n = /* @__PURE__ */ new Set([
|
|
339
425
|
"static",
|
|
340
426
|
"relative",
|
|
341
427
|
"absolute",
|
|
@@ -345,116 +431,71 @@ function E() {
|
|
|
345
431
|
"revert",
|
|
346
432
|
"revert-layer"
|
|
347
433
|
]), c = e.getPropertyValue("position").trim().toLowerCase();
|
|
348
|
-
c && !
|
|
434
|
+
c && !n.has(c) && e.removeProperty("position");
|
|
349
435
|
const d = /* @__PURE__ */ new Set(["initial", "inherit", "unset", "revert", "revert-layer"]), o = e.getPropertyValue("opacity").trim().toLowerCase();
|
|
350
436
|
o && !d.has(o) && (o.endsWith("%") ? parseFloat(o) / 100 : parseFloat(o)) < 0.05 && e.removeProperty("opacity");
|
|
351
|
-
for (const i of
|
|
437
|
+
for (const i of k) {
|
|
352
438
|
const l = e.getPropertyValue(i);
|
|
353
|
-
l &&
|
|
439
|
+
l && E.test(l) && e.removeProperty(i);
|
|
354
440
|
}
|
|
355
441
|
e.length === 0 && t.removeAttribute("style");
|
|
356
|
-
}),
|
|
357
|
-
if (!
|
|
442
|
+
}), a.addHook("uponSanitizeAttribute", (t, e) => {
|
|
443
|
+
if (!S.has(e.attrName)) return;
|
|
358
444
|
const r = e.attrValue;
|
|
359
445
|
/^\s*\/\//.test(r) ? (e.attrValue = r.trimStart().replace(/^\/\//, "https://"), e.keepAttr = !0) : /^\s*\\/.test(r) && (e.keepAttr = !1);
|
|
360
|
-
}),
|
|
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) => {
|
|
361
456
|
if (!(t instanceof Element) || !t.hasAttribute("srcset")) return;
|
|
362
|
-
const r = (t.getAttribute("srcset") ?? "").split(","),
|
|
363
|
-
if (r.some((o) => /^\s*\\/.test(
|
|
457
|
+
const r = (t.getAttribute("srcset") ?? "").split(","), n = (o) => o.trim().split(/\s+/)[0];
|
|
458
|
+
if (r.some((o) => /^\s*\\/.test(n(o)))) {
|
|
364
459
|
t.removeAttribute("srcset");
|
|
365
460
|
return;
|
|
366
461
|
}
|
|
367
462
|
let c = !1;
|
|
368
463
|
const d = r.map((o) => {
|
|
369
|
-
const i =
|
|
464
|
+
const i = n(o);
|
|
370
465
|
if (!i.startsWith("//")) return o;
|
|
371
466
|
c = !0;
|
|
372
467
|
const l = o.indexOf(i);
|
|
373
468
|
return o.slice(0, l) + "https://" + i.slice(2) + o.slice(l + i.length);
|
|
374
469
|
});
|
|
375
470
|
c && t.setAttribute("srcset", d.join(","));
|
|
376
|
-
}),
|
|
377
|
-
var
|
|
378
|
-
if (!(t instanceof Element) || t.tagName !== "A" && t.tagName !== "AREA" || ((
|
|
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;
|
|
379
474
|
const e = t.getAttribute("rel") ?? "", r = new Set(e.split(/\s+/).filter(Boolean));
|
|
380
475
|
r.add("noopener"), t.setAttribute("rel", [...r].join(" "));
|
|
381
|
-
}),
|
|
476
|
+
}), a.addHook("afterSanitizeAttributes", (t) => {
|
|
382
477
|
if (!(t instanceof Element) || t.tagName !== "IFRAME" || !t.hasAttribute("sandbox")) return;
|
|
383
|
-
const r = (t.getAttribute("sandbox") ?? "").toLowerCase().split(/\s+/).filter(Boolean),
|
|
384
|
-
|
|
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) => {
|
|
481
|
+
if (!(t instanceof Element) || t.tagName !== "PARAM") return;
|
|
482
|
+
const e = t.getAttribute("value");
|
|
483
|
+
e && /^\s*(?:javascript|vbscript):/i.test(e) && t.removeAttribute("value");
|
|
385
484
|
});
|
|
386
|
-
const
|
|
387
|
-
return
|
|
485
|
+
const s = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
|
|
486
|
+
return a.addHook("afterSanitizeElements", (t) => {
|
|
388
487
|
if (!(t instanceof Element) || t.tagName.toLowerCase() !== "annotation-xml") return;
|
|
389
488
|
const e = (t.getAttribute("encoding") ?? "").toLowerCase().trim();
|
|
390
|
-
|
|
391
|
-
}),
|
|
489
|
+
s.has(e) && t.remove();
|
|
490
|
+
}), a;
|
|
392
491
|
}
|
|
393
|
-
function
|
|
492
|
+
function N(s, t) {
|
|
394
493
|
if (typeof window > "u")
|
|
395
494
|
throw new Error("sanitizeHtml requires a DOM environment (window is not defined)");
|
|
396
|
-
const e = t != null && t.allowFormAttributeNames ? { ...
|
|
397
|
-
return
|
|
398
|
-
}
|
|
399
|
-
const m = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), u = "http://platform-sanitize.invalid/", g = /^\s*\/\//, b = (
|
|
400
|
-
// oxlint-disable-next-line no-control-regex -- intentional security guard
|
|
401
|
-
/^[\u0000-\u0020\u007F-\u00A0\u2000-\u200F\u2028\u2029\u202F\u205F\u2060\u3000\uFEFF]*(?:javascript|data|vbscript|file):/i
|
|
402
|
-
), S = {
|
|
403
|
-
colon: ":",
|
|
404
|
-
// javascript:alert(1) → javascript:alert(1)
|
|
405
|
-
sol: "/",
|
|
406
|
-
// //evil.com → //evil.com (protocol-relative)
|
|
407
|
-
bsol: "\\",
|
|
408
|
-
// \\evil.com → \\evil.com
|
|
409
|
-
Tab: " ",
|
|
410
|
-
// java	script: — WHATWG strips mid-scheme tabs
|
|
411
|
-
NewLine: `
|
|
412
|
-
`
|
|
413
|
-
// java
script:
|
|
414
|
-
};
|
|
415
|
-
function x(n) {
|
|
416
|
-
return n.replace(/&#(\d+);/g, (t, e) => {
|
|
417
|
-
const r = Number(e);
|
|
418
|
-
if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
|
|
419
|
-
try {
|
|
420
|
-
return String.fromCodePoint(r);
|
|
421
|
-
} catch {
|
|
422
|
-
return "";
|
|
423
|
-
}
|
|
424
|
-
}).replace(/&#x([\da-f]+);/gi, (t, e) => {
|
|
425
|
-
const r = parseInt(e, 16);
|
|
426
|
-
if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
|
|
427
|
-
try {
|
|
428
|
-
return String.fromCodePoint(r);
|
|
429
|
-
} catch {
|
|
430
|
-
return "";
|
|
431
|
-
}
|
|
432
|
-
}).replace(/&([A-Za-z][A-Za-z0-9]*);/g, (t, e) => S[e] ?? t);
|
|
433
|
-
}
|
|
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";
|
|
438
|
-
if (/&[#A-Za-z]/.test(t)) {
|
|
439
|
-
const e = x(t);
|
|
440
|
-
if (g.test(e) || b.test(e))
|
|
441
|
-
return "about:blank";
|
|
442
|
-
try {
|
|
443
|
-
const r = new URL(e, u);
|
|
444
|
-
if (!r.href.startsWith(u) && !m.has(r.protocol))
|
|
445
|
-
return "about:blank";
|
|
446
|
-
} catch {
|
|
447
|
-
return "about:blank";
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
try {
|
|
451
|
-
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, "");
|
|
453
|
-
} catch {
|
|
454
|
-
return "about:blank";
|
|
455
|
-
}
|
|
495
|
+
const e = t != null && t.allowFormAttributeNames ? { ...g, SANITIZE_DOM: !1 } : g;
|
|
496
|
+
return R().sanitize(s ?? "", e);
|
|
456
497
|
}
|
|
457
498
|
export {
|
|
458
|
-
|
|
459
|
-
|
|
499
|
+
N as sanitizeHtml,
|
|
500
|
+
A as sanitizeUrl
|
|
460
501
|
};
|
|
@@ -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":"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"}
|