@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 +123 -73
- package/dist/sanitizeHtml.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
const
|
|
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
|
-
]),
|
|
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
|
-
]),
|
|
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
|
-
]),
|
|
269
|
-
|
|
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
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
323
|
-
const
|
|
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
|
-
]),
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
for (const
|
|
337
|
-
const
|
|
338
|
-
|
|
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
|
-
|
|
341
|
-
}),
|
|
342
|
-
if (!
|
|
343
|
-
const
|
|
344
|
-
/^\s*\/\//.test(
|
|
345
|
-
}),
|
|
346
|
-
if (!(
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
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
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
if (!
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
return
|
|
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
|
-
|
|
361
|
-
}),
|
|
362
|
-
var
|
|
363
|
-
if (!(
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
}),
|
|
367
|
-
if (!(
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
}),
|
|
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
|
|
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 ? { ...
|
|
376
|
-
return
|
|
425
|
+
const e = t != null && t.allowFormAttributeNames ? { ...p, SANITIZE_DOM: !1 } : p;
|
|
426
|
+
return E().sanitize(a ?? "", e);
|
|
377
427
|
}
|
|
378
|
-
const
|
|
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
script:
|
|
393
443
|
};
|
|
394
|
-
function
|
|
395
|
-
return
|
|
396
|
-
const
|
|
397
|
-
if (!Number.isFinite(
|
|
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(
|
|
449
|
+
return String.fromCodePoint(r);
|
|
400
450
|
} catch {
|
|
401
451
|
return "";
|
|
402
452
|
}
|
|
403
453
|
}).replace(/&#x([\da-f]+);/gi, (t, e) => {
|
|
404
|
-
const
|
|
405
|
-
if (!Number.isFinite(
|
|
454
|
+
const r = parseInt(e, 16);
|
|
455
|
+
if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
|
|
406
456
|
try {
|
|
407
|
-
return String.fromCodePoint(
|
|
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
|
|
414
|
-
if (!
|
|
415
|
-
const t =
|
|
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 =
|
|
468
|
+
const e = x(t);
|
|
419
469
|
if (b.test(e) || g.test(e))
|
|
420
470
|
return "about:blank";
|
|
421
471
|
try {
|
|
422
|
-
const
|
|
423
|
-
if (!
|
|
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,
|
|
431
|
-
return !
|
|
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
|
-
|
|
438
|
-
|
|
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":"
|
|
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"}
|