From fda66d27916b716ae1a8ba0f061a3ca919c45d97 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Wed, 24 Jun 2026 10:07:12 -0700 Subject: [PATCH 1/4] Sanitizer: block data:/vbscript: URLs in allowed attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sanitizer's SAFE_URL_PATTERN only rejected javascript:, so a data:text/html (or vbscript:) URL in an href/src passed the allowList — an XSS vector via data-bs-title/data-bs-content. Reject data: and vbscript: in SAFE_URL_PATTERN and re-allow only safe base64 image/video/ audio data URLs via a restored DATA_URL_PATTERN. Fixes #42443. --- js/src/util/sanitizer.js | 12 ++++++++++-- js/tests/unit/util/sanitizer.spec.js | 8 +++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js index cb33633ffd39..afd9b61edf81 100644 --- a/js/src/util/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -63,14 +63,22 @@ const uriAttributes = new Set([ * * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 */ -const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i +const SAFE_URL_PATTERN = /^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i + +/** + * A pattern that matches safe data URLs. Only matches image, video and audio + * types — notably NOT `data:text/html`, which is an XSS vector. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L49 + */ +const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z0-9=]+$/i const allowedAttribute = (attribute, allowedAttributeList) => { const attributeName = attribute.nodeName.toLowerCase() if (allowedAttributeList.includes(attributeName)) { if (uriAttributes.has(attributeName)) { - return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue)) + return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue)) } return true diff --git a/js/tests/unit/util/sanitizer.spec.js b/js/tests/unit/util/sanitizer.spec.js index 2b21ef2e1967..ccf5c5cf6dd4 100644 --- a/js/tests/unit/util/sanitizer.spec.js +++ b/js/tests/unit/util/sanitizer.spec.js @@ -67,7 +67,13 @@ describe('Sanitizer', () => { 'jav\u0000ascript:alert();' ] - for (const url of invalidUrls) { + const dangerousDataUrls = [ + 'data:text/html,hello', + 'data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==', + 'vbscript:msgbox(1)' + ] + + for (const url of [...invalidUrls, ...dangerousDataUrls]) { const template = [ '
', ` Click me`, From c4cf81e372e680d464ebc65af1ec7b44ea1cb450 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Thu, 25 Jun 2026 12:50:35 -0700 Subject: [PATCH 2/4] Bump bundlewatch size thresholds --- .bundlewatch.config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 2c89f7ea47a8..eb14781be686 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -34,7 +34,7 @@ }, { "path": "./dist/js/bootstrap.bundle.js", - "maxSize": "82.5 kB" + "maxSize": "82.75 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js", @@ -42,7 +42,7 @@ }, { "path": "./dist/js/bootstrap.js", - "maxSize": "53.75 kB" + "maxSize": "54.0 kB" }, { "path": "./dist/js/bootstrap.min.js", From ffb5db1a09bc17edd984fb88a293e37cbb1fff70 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Sat, 27 Jun 2026 14:49:55 -0700 Subject: [PATCH 3/4] Sanitizer: drop redundant 0-9 from base64 data-URI char class \d already matches 0-9, so the explicit 0-9 in the same class was dead weight. Functionally identical; clears the CodeQL overly-permissive-range alert on the overlap. --- js/src/util/sanitizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js index afd9b61edf81..f0482eb94ed9 100644 --- a/js/src/util/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -71,7 +71,7 @@ const SAFE_URL_PATTERN = /^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^& * * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L49 */ -const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z0-9=]+$/i +const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i const allowedAttribute = (attribute, allowedAttributeList) => { const attributeName = attribute.nodeName.toLowerCase() From 65bc4561a2f80c1b09e92d149970eb3c33643451 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Sat, 27 Jun 2026 14:54:54 -0700 Subject: [PATCH 4/4] Build: bump bundle.js bundlewatch threshold for sanitizer additions --- .bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 8986f6b95373..e8991a633756 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -34,7 +34,7 @@ }, { "path": "./dist/js/bootstrap.bundle.js", - "maxSize": "83.5 kB" + "maxSize": "83.75 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js",