@37signals/lexxy 0.9.31-beta → 0.9.31
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/lexxy.esm.js +60 -6
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -129,7 +129,8 @@ var Lexxy = {
|
|
|
129
129
|
// Until 3.3.2 that never came up: attributes admitted by a *functional* ADD_ATTR
|
|
130
130
|
// skipped URI validation entirely (GHSA-cjmm-f4jc-qw8r), which is how data:
|
|
131
131
|
// URLs worked here — and, less happily, how `url="javascript:…"` survived too.
|
|
132
|
-
// The fix restored validation for both.
|
|
132
|
+
// The fix restored validation for both. The bypass was reported to us by
|
|
133
|
+
// @petitpois via HackerOne.
|
|
133
134
|
//
|
|
134
135
|
// So `url` is marked URI-safe, which hands the decision to this hook. The hook
|
|
135
136
|
// only ever removes an attribute — it never force-keeps one — so scoping stays
|
|
@@ -152,9 +153,41 @@ var Lexxy = {
|
|
|
152
153
|
// ordinary prose like "Q4: results" is not a URI any regex here would accept.
|
|
153
154
|
const URI_BEARING_ATTACHMENT_ATTRIBUTES = [ "url" ];
|
|
154
155
|
|
|
155
|
-
// DOMPurify's own IS_ALLOWED_URI, reproduced rather than narrowed, so
|
|
156
|
-
// non-attachment tag is treated exactly as DOMPurify would have treated
|
|
157
|
-
|
|
156
|
+
// DOMPurify's own IS_ALLOWED_URI scheme list, reproduced rather than narrowed, so
|
|
157
|
+
// `url` on a non-attachment tag is treated exactly as DOMPurify would have treated
|
|
158
|
+
// it. Kept as a source string so allowedUriRegexp() can widen it with an editor's
|
|
159
|
+
// declared schemes without re-deriving the rest of the pattern.
|
|
160
|
+
const BASE_URI_SCHEMES = "(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix";
|
|
161
|
+
|
|
162
|
+
// A scheme name (not a full pattern), so a caller can't inject regexp syntax.
|
|
163
|
+
const SCHEME_NAME = /^[a-z][a-z0-9+.-]*$/;
|
|
164
|
+
|
|
165
|
+
// The executable schemes, by DOMPurify's own IS_SCRIPT_OR_DATA definition
|
|
166
|
+
// (/^(?:\w+script|data):/i): javascript, vbscript, …script, and data. Widening the
|
|
167
|
+
// allowlist to admit one would let it survive on href/object[data], which the
|
|
168
|
+
// override otherwise defeats — DOMPurify does not re-block a scheme its
|
|
169
|
+
// ALLOWED_URI_REGEXP accepts. A caller declaring one is dropped, so it stays
|
|
170
|
+
// refused. This is a closed set (the schemes a browser executes), not a growing
|
|
171
|
+
// denylist.
|
|
172
|
+
const EXECUTABLE_SCHEME = /^(?:\w+script|data)$/i;
|
|
173
|
+
|
|
174
|
+
// Builds DOMPurify's default IS_ALLOWED_URI, optionally with extra schemes folded
|
|
175
|
+
// into the scheme alternation. Widening the recognised-safe scheme set is how a
|
|
176
|
+
// custom-scheme identifier (a mention's `gid://…`) passes validation without
|
|
177
|
+
// exempting any attribute from it. Passing no schemes reproduces DOMPurify's
|
|
178
|
+
// default exactly.
|
|
179
|
+
function allowedUriRegexp(extraSchemes = []) {
|
|
180
|
+
const extra = extraSchemes
|
|
181
|
+
.map(scheme => String(scheme).toLowerCase())
|
|
182
|
+
.filter(scheme => SCHEME_NAME.test(scheme) && !EXECUTABLE_SCHEME.test(scheme))
|
|
183
|
+
.map(scheme => scheme.replace(/[.+-]/g, "\\$&"));
|
|
184
|
+
|
|
185
|
+
const schemes = [ BASE_URI_SCHEMES, ...extra ].join("|");
|
|
186
|
+
|
|
187
|
+
return new RegExp(`^(?:(?:${schemes}):|[^a-z]|[a-z+.-]+(?:[^a-z+.:-]|$))`, "i")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const ALLOWED_URI = allowedUriRegexp();
|
|
158
191
|
|
|
159
192
|
// eslint-disable-next-line no-control-regex -- mirrors DOMPurify's own ATTR_WHITESPACE
|
|
160
193
|
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g;
|
|
@@ -351,7 +384,17 @@ function buildConfig(allowedElements = null) {
|
|
|
351
384
|
// default tag and attribute policy stands. `ALLOWED_TAGS: []` would not be a
|
|
352
385
|
// default, it would be a refusal: it strips every tag. An editor that declares
|
|
353
386
|
// an empty allowlist still gets that refusal, because it asked for it.
|
|
354
|
-
|
|
387
|
+
//
|
|
388
|
+
// uriSafeSchemes widens the scheme validation rather than exempting an attribute
|
|
389
|
+
// from it: ALLOWED_URI_REGEXP is set only when an editor declares custom schemes,
|
|
390
|
+
// so a value like a mention's `gid://…` passes while javascript:/data: stay
|
|
391
|
+
// refused on every attribute — including href and object[data]. Nothing is taken
|
|
392
|
+
// out of URI checking, so there is no attribute to guard against exempting.
|
|
393
|
+
if (allowedElements) {
|
|
394
|
+
const { uriSafeSchemes, ...tagPolicy } = allowlistFor(allowedElements);
|
|
395
|
+
if (uriSafeSchemes.length) config.ALLOWED_URI_REGEXP = allowedUriRegexp(uriSafeSchemes);
|
|
396
|
+
Object.assign(config, tagPolicy);
|
|
397
|
+
}
|
|
355
398
|
|
|
356
399
|
// Always assigned, including when we have no policy — `null` is what
|
|
357
400
|
// trustedTypesPolicy() returns then, and `TRUSTED_TYPES_POLICY: null` is
|
|
@@ -374,6 +417,7 @@ function allowlistFor(allowedElements) {
|
|
|
374
417
|
// Object.prototype key: `tagAttributes["constructor"]` would answer with a
|
|
375
418
|
// function, and ADD_ATTR would call .includes on it.
|
|
376
419
|
const tagAttributes = Object.create(null);
|
|
420
|
+
const uriSafeSchemes = [];
|
|
377
421
|
|
|
378
422
|
// Lowercased, because DOMPurify lowercases ALLOWED_TAGS and calls ADD_ATTR
|
|
379
423
|
// with the lowercased tag and attribute names. Keeping the caller's casing
|
|
@@ -386,6 +430,15 @@ function allowlistFor(allowedElements) {
|
|
|
386
430
|
|
|
387
431
|
tagAttributes[tag] ||= [];
|
|
388
432
|
tagAttributes[tag].push(...attributes);
|
|
433
|
+
|
|
434
|
+
// A custom scheme a caller declares is folded into the editor's URI-scheme
|
|
435
|
+
// allowlist (ALLOWED_URI_REGEXP in buildConfig), so a value like a mention's
|
|
436
|
+
// `gid="gid://…"` — which DOMPurify otherwise drops as an unknown scheme, even
|
|
437
|
+
// once the `gid` name is allowed — passes validation. It widens the recognised
|
|
438
|
+
// schemes, it does not exempt an attribute: javascript:/data: stay refused
|
|
439
|
+
// everywhere, so there is no navigational attribute to guard. Declared per
|
|
440
|
+
// element for locality, but a scheme is editor-wide once allowed.
|
|
441
|
+
uriSafeSchemes.push(...(element.uriSafeSchemes ?? []));
|
|
389
442
|
}
|
|
390
443
|
|
|
391
444
|
// Only for tags the caller already permits — this widens what an allowed
|
|
@@ -397,7 +450,8 @@ function allowlistFor(allowedElements) {
|
|
|
397
450
|
return {
|
|
398
451
|
ALLOWED_TAGS: Object.keys(tagAttributes),
|
|
399
452
|
ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES,
|
|
400
|
-
ADD_ATTR: (attribute, tag) => tagAttributes[tag]?.includes(attribute)
|
|
453
|
+
ADD_ATTR: (attribute, tag) => tagAttributes[tag]?.includes(attribute),
|
|
454
|
+
uriSafeSchemes
|
|
401
455
|
}
|
|
402
456
|
}
|
|
403
457
|
|