@instructure/platform-sanitize 0.3.2 → 0.3.11

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,5 +1,5 @@
1
- import a from "dompurify";
2
- const d = /* @__PURE__ */ new Set([
1
+ import c from "dompurify";
2
+ const g = /* @__PURE__ */ new Set([
3
3
  // layout
4
4
  "display",
5
5
  "float",
@@ -12,6 +12,12 @@ const d = /* @__PURE__ */ new Set([
12
12
  "direction",
13
13
  "user-select",
14
14
  "zoom",
15
+ // positioning — values filtered by allowlist hook below
16
+ "position",
17
+ "top",
18
+ "right",
19
+ "bottom",
20
+ "left",
15
21
  // sizing
16
22
  "width",
17
23
  "height",
@@ -19,6 +25,12 @@ const d = /* @__PURE__ */ new Set([
19
25
  "min-height",
20
26
  "max-width",
21
27
  "max-height",
28
+ // box model extensions
29
+ "box-sizing",
30
+ "aspect-ratio",
31
+ "resize",
32
+ "object-fit",
33
+ "object-position",
22
34
  // spacing
23
35
  "margin",
24
36
  "margin-top",
@@ -46,6 +58,35 @@ const d = /* @__PURE__ */ new Set([
46
58
  "text-indent",
47
59
  "white-space",
48
60
  "vertical-align",
61
+ "text-transform",
62
+ "letter-spacing",
63
+ "word-spacing",
64
+ "text-shadow",
65
+ "text-overflow",
66
+ "text-decoration-color",
67
+ "text-decoration-line",
68
+ "text-decoration-style",
69
+ "text-decoration-thickness",
70
+ "text-underline-offset",
71
+ "text-align-last",
72
+ "unicode-bidi",
73
+ // font extensions
74
+ "font-feature-settings",
75
+ "font-kerning",
76
+ "font-optical-sizing",
77
+ "font-variant-caps",
78
+ "font-variant-ligatures",
79
+ "font-variant-numeric",
80
+ // text flow
81
+ "word-break",
82
+ "overflow-wrap",
83
+ "word-wrap",
84
+ "hyphens",
85
+ "line-break",
86
+ "tab-size",
87
+ "writing-mode",
88
+ "text-orientation",
89
+ "text-combine-upright",
49
90
  // color & background
50
91
  "color",
51
92
  "background",
@@ -56,6 +97,9 @@ const d = /* @__PURE__ */ new Set([
56
97
  "background-position-x",
57
98
  "background-position-y",
58
99
  "background-repeat",
100
+ "background-size",
101
+ "background-clip",
102
+ "background-origin",
59
103
  // border
60
104
  "border",
61
105
  "border-color",
@@ -80,6 +124,12 @@ const d = /* @__PURE__ */ new Set([
80
124
  "border-left-color",
81
125
  "border-left-style",
82
126
  "border-left-width",
127
+ // shadows & outline
128
+ "box-shadow",
129
+ "outline",
130
+ "outline-color",
131
+ "outline-style",
132
+ "outline-width",
83
133
  // list
84
134
  "list-style",
85
135
  "list-style-image",
@@ -87,6 +137,8 @@ const d = /* @__PURE__ */ new Set([
87
137
  "list-style-type",
88
138
  // table
89
139
  "table-layout",
140
+ "caption-side",
141
+ "empty-cells",
90
142
  // flex
91
143
  "flex",
92
144
  "flex-basis",
@@ -126,8 +178,54 @@ const d = /* @__PURE__ */ new Set([
126
178
  "grid-template",
127
179
  "grid-template-areas",
128
180
  "grid-template-columns",
129
- "grid-template-rows"
130
- ]), c = /* @__PURE__ */ new Set([
181
+ "grid-template-rows",
182
+ // transitions
183
+ "transition",
184
+ "transition-property",
185
+ "transition-duration",
186
+ "transition-timing-function",
187
+ "transition-delay",
188
+ // animations
189
+ "animation",
190
+ "animation-name",
191
+ "animation-duration",
192
+ "animation-timing-function",
193
+ "animation-delay",
194
+ "animation-iteration-count",
195
+ "animation-direction",
196
+ "animation-fill-mode",
197
+ "animation-play-state",
198
+ // columns
199
+ "column-count",
200
+ "column-width",
201
+ "columns",
202
+ "column-rule",
203
+ "column-rule-color",
204
+ "column-rule-style",
205
+ "column-rule-width",
206
+ "column-fill",
207
+ "column-span",
208
+ // page breaks
209
+ "break-before",
210
+ "break-after",
211
+ "break-inside",
212
+ "page-break-before",
213
+ "page-break-after",
214
+ "page-break-inside",
215
+ // generated content
216
+ "quotes",
217
+ "counter-reset",
218
+ "counter-increment",
219
+ "content",
220
+ // UI / interaction
221
+ // pointer-events is included: RCE uses it legitimately (e.g. non-interactive
222
+ // decorative overlays). It is not in the overlay-phishing class because it
223
+ // cannot reposition elements — position/z-index remain blocked.
224
+ "pointer-events",
225
+ "caret-color",
226
+ "accent-color",
227
+ "appearance"
228
+ ]), b = /* @__PURE__ */ new Set([
131
229
  "src",
132
230
  "href",
133
231
  "action",
@@ -138,7 +236,29 @@ const d = /* @__PURE__ */ new Set([
138
236
  "cite",
139
237
  "longdesc",
140
238
  "xlink:href"
141
- ]), l = /^\s*(\/\/|\\)/, f = {
239
+ ]), u = /^\s*(\/\/|\\)/, m = [
240
+ "background",
241
+ "background-image",
242
+ "list-style",
243
+ "list-style-image",
244
+ "cursor",
245
+ // content: url(...) triggers an HTTP GET even on non-pseudo elements in some
246
+ // browsers; strip it as defense-in-depth against tracking-pixel exfiltration.
247
+ "content"
248
+ ], w = /url\s*\(\s*['"]?(?:[a-z][a-z0-9+\-.]*:|\/\/)/i, h = /* @__PURE__ */ new Set([
249
+ "allow-downloads",
250
+ "allow-forms",
251
+ "allow-modals",
252
+ "allow-orientation-lock",
253
+ "allow-pointer-lock",
254
+ "allow-popups",
255
+ "allow-popups-to-escape-sandbox",
256
+ "allow-presentation",
257
+ "allow-same-origin",
258
+ "allow-scripts",
259
+ "allow-storage-access-by-user-activation",
260
+ "allow-top-navigation-by-user-activation"
261
+ ]), y = {
142
262
  ADD_TAGS: ["iframe"],
143
263
  ADD_ATTR: [
144
264
  "allowfullscreen",
@@ -153,7 +273,13 @@ const d = /* @__PURE__ */ new Set([
153
273
  "target",
154
274
  "webkitallowfullscreen",
155
275
  "mozallowfullscreen",
156
- "scrolling"
276
+ "scrolling",
277
+ // MathML 4 (W3C) annotation attributes for screen-reader accessibility.
278
+ // MathCAT, JAWS, and NVDA use `intent` to know how to pronounce
279
+ // expressions; `arg` labels sub-expressions referenced by intent.
280
+ // These are plain string annotations — no URL-loading, no code execution.
281
+ "intent",
282
+ "arg"
157
283
  ],
158
284
  // Rails UJS turns data-method/data-remote/etc. on clickable elements into
159
285
  // state-changing requests carrying the victim's CSRF token. Strip them so
@@ -169,44 +295,117 @@ const d = /* @__PURE__ */ new Set([
169
295
  // a per-call cast. Do not perform string operations (.slice, .match, etc.)
170
296
  // on the return value — those methods do not exist on TrustedHTML and will
171
297
  // throw in modern browsers.
172
- RETURN_TRUSTED_TYPE: !0
298
+ RETURN_TRUSTED_TYPE: !0,
299
+ // Wraps input in a body context during parsing, preventing namespace
300
+ // confusion attacks where fragments like <svg> could influence parse context.
301
+ FORCE_BODY: !0
173
302
  };
174
- let o = null;
175
- function g() {
176
- return o || (o = typeof a == "function" ? a(window) : a, o.addHook("afterSanitizeAttributes", (t) => {
303
+ let i = null;
304
+ function k() {
305
+ return i || (i = typeof c == "function" ? c(window) : c, i.addHook("afterSanitizeAttributes", (t) => {
177
306
  if (!(t instanceof Element) || !t.hasAttribute("style")) return;
178
- const e = t.style, i = [];
179
- for (let r = 0; r < e.length; r++) {
180
- const n = e.item(r);
181
- d.has(n) || i.push(n);
307
+ const e = t.style, o = [];
308
+ for (let n = 0; n < e.length; n++) {
309
+ const a = e.item(n);
310
+ g.has(a) || o.push(a);
311
+ }
312
+ for (const n of o) e.removeProperty(n);
313
+ const r = /* @__PURE__ */ new Set([
314
+ "static",
315
+ "relative",
316
+ "absolute",
317
+ "initial",
318
+ "inherit",
319
+ "unset",
320
+ "revert",
321
+ "revert-layer"
322
+ ]), s = e.getPropertyValue("position").trim().toLowerCase();
323
+ s && !r.has(s) && e.removeProperty("position");
324
+ for (const n of m) {
325
+ const a = e.getPropertyValue(n);
326
+ a && w.test(a) && e.removeProperty(n);
182
327
  }
183
- for (const r of i) e.removeProperty(r);
184
328
  e.length === 0 && t.removeAttribute("style");
185
- }), o.addHook("uponSanitizeAttribute", (t, e) => {
186
- c.has(e.attrName) && l.test(e.attrValue) && (e.keepAttr = !1);
187
- }), o.addHook("afterSanitizeAttributes", (t) => {
329
+ }), i.addHook("uponSanitizeAttribute", (t, e) => {
330
+ b.has(e.attrName) && u.test(e.attrValue) && (e.keepAttr = !1);
331
+ }), i.addHook("afterSanitizeAttributes", (t) => {
188
332
  if (!(t instanceof Element) || !t.hasAttribute("srcset")) return;
189
- (t.getAttribute("srcset") ?? "").split(",").map((r) => r.trim().split(/\s+/)[0]).some((r) => l.test(r)) && t.removeAttribute("srcset");
190
- }), o);
333
+ (t.getAttribute("srcset") ?? "").split(",").map((r) => r.trim().split(/\s+/)[0]).some((r) => u.test(r)) && t.removeAttribute("srcset");
334
+ }), i.addHook("afterSanitizeAttributes", (t) => {
335
+ var r;
336
+ if (!(t instanceof Element) || t.tagName !== "A" && t.tagName !== "AREA" || ((r = t.getAttribute("target")) == null ? void 0 : r.toLowerCase()) !== "_blank") return;
337
+ const e = t.getAttribute("rel") ?? "", o = new Set(e.split(/\s+/).filter(Boolean));
338
+ o.add("noopener"), t.setAttribute("rel", [...o].join(" "));
339
+ }), i.addHook("afterSanitizeAttributes", (t) => {
340
+ if (!(t instanceof Element) || t.tagName !== "IFRAME" || !t.hasAttribute("sandbox")) return;
341
+ const o = (t.getAttribute("sandbox") ?? "").toLowerCase().split(/\s+/).filter(Boolean), r = o.filter((s) => h.has(s));
342
+ r.length !== o.length && t.setAttribute("sandbox", r.join(" "));
343
+ }), i);
191
344
  }
192
- function p(t) {
345
+ function R(t) {
193
346
  if (typeof window > "u")
194
347
  throw new Error("sanitizeHtml requires a DOM environment (window is not defined)");
195
- return g().sanitize(t ?? "", f);
348
+ return k().sanitize(t ?? "", y);
196
349
  }
197
- const m = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), s = "http://platform-sanitize.invalid/", u = /^\s*\/\//;
198
- function h(t) {
350
+ const d = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]), l = "http://platform-sanitize.invalid/", f = /^\s*\/\//, p = (
351
+ // oxlint-disable-next-line no-control-regex -- intentional security guard
352
+ /^[\u0000-\u0020\u007F-\u00A0\u2000-\u200F\u2028\u2029\u202F\u205F\u2060\u3000\uFEFF]*(?:javascript|data|vbscript|file):/i
353
+ ), A = {
354
+ colon: ":",
355
+ // javascript&colon;alert(1) → javascript:alert(1)
356
+ sol: "/",
357
+ // &sol;&sol;evil.com → //evil.com (protocol-relative)
358
+ bsol: "\\",
359
+ // &bsol;&bsol;evil.com → \\evil.com
360
+ Tab: " ",
361
+ // java&Tab;script: — WHATWG strips mid-scheme tabs
362
+ NewLine: `
363
+ `
364
+ // java&NewLine;script:
365
+ };
366
+ function E(t) {
367
+ return t.replace(/&#(\d+);/g, (e, o) => {
368
+ const r = Number(o);
369
+ if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
370
+ try {
371
+ return String.fromCodePoint(r);
372
+ } catch {
373
+ return "";
374
+ }
375
+ }).replace(/&#x([\da-f]+);/gi, (e, o) => {
376
+ const r = parseInt(o, 16);
377
+ if (!Number.isFinite(r) || r < 0 || r > 1114111) return "";
378
+ try {
379
+ return String.fromCodePoint(r);
380
+ } catch {
381
+ return "";
382
+ }
383
+ }).replace(/&([A-Za-z][A-Za-z0-9]*);/g, (e, o) => A[o] ?? e);
384
+ }
385
+ function S(t) {
199
386
  if (!t || !t.trim()) return "about:blank";
200
387
  const e = t.replace(/\\/g, "/");
201
- if (u.test(e)) return "about:blank";
388
+ if (f.test(e) || p.test(e)) return "about:blank";
389
+ if (/&[#A-Za-z]/.test(e)) {
390
+ const o = E(e);
391
+ if (f.test(o) || p.test(o))
392
+ return "about:blank";
393
+ try {
394
+ const r = new URL(o, l);
395
+ if (!r.href.startsWith(l) && !d.has(r.protocol))
396
+ return "about:blank";
397
+ } catch {
398
+ return "about:blank";
399
+ }
400
+ }
202
401
  try {
203
- const i = new URL(e, s);
204
- return m.has(i.protocol) ? i.href.startsWith(s) ? t : e.replace(/[\x00-\x1F]/g, "") : "about:blank";
402
+ const o = new URL(e, l);
403
+ return !d.has(o.protocol) || (o.protocol === "http:" || o.protocol === "https:") && (o.username || o.password) ? "about:blank" : o.href.startsWith(l) ? t : e.replace(/[\x00-\x1F\u2028\u2029]/g, "").replace(/%250[9ad]/gi, "").replace(/%0[9ad]/gi, "");
205
404
  } catch {
206
405
  return "about:blank";
207
406
  }
208
407
  }
209
408
  export {
210
- p as sanitizeHtml,
211
- h as sanitizeUrl
409
+ R as sanitizeHtml,
410
+ S as sanitizeUrl
212
411
  };
@@ -1 +1 @@
1
- {"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../src/sanitizeHtml.ts"],"names":[],"mappings":"AA4OA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAKpE"}
1
+ {"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../src/sanitizeHtml.ts"],"names":[],"mappings":"AA6aA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAKpE"}
@@ -1 +1 @@
1
- {"version":3,"file":"sanitizeUrl.d.ts","sourceRoot":"","sources":["../src/sanitizeUrl.ts"],"names":[],"mappings":"AAyBA,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAoBlE"}
1
+ {"version":3,"file":"sanitizeUrl.d.ts","sourceRoot":"","sources":["../src/sanitizeUrl.ts"],"names":[],"mappings":"AAoFA,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAwDlE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/platform-sanitize",
3
- "version": "0.3.2",
3
+ "version": "0.3.11",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -17,24 +17,25 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
+ "scripts": {
21
+ "build": "vite build",
22
+ "dev": "vite build --watch",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "test:coverage": "vitest run --coverage",
26
+ "type-check": "tsc --noEmit"
27
+ },
20
28
  "peerDependencies": {
21
- "dompurify": "^3.0.0"
29
+ "dompurify": "^3.4.0"
22
30
  },
23
31
  "devDependencies": {
32
+ "@types/trusted-types": "^2.0.7",
24
33
  "@vitest/coverage-v8": "^4.0.17",
25
- "dompurify": "^3.0.0",
34
+ "dompurify": "^3.4.0",
26
35
  "jsdom": "^25.0.0",
27
36
  "typescript": "^5.3.0",
28
37
  "vite": "^6.0.0",
29
38
  "vite-plugin-dts": "^4.0.0",
30
39
  "vitest": "^4.0.0"
31
- },
32
- "scripts": {
33
- "build": "vite build",
34
- "dev": "vite build --watch",
35
- "test": "vitest run",
36
- "test:watch": "vitest",
37
- "test:coverage": "vitest run --coverage",
38
- "type-check": "tsc --noEmit"
39
40
  }
40
- }
41
+ }