From 538b4ef33a551503d93756c4be2bd4b6c72f72df Mon Sep 17 00:00:00 2001
From: Aljo Joby <141745680+aljojoby9@users.noreply.github.com>
Date: Fri, 7 Aug 2026 00:56:19 +0530
Subject: [PATCH 1/3] Sanitizer: block data:/vbscript: URLs (XSS hardening)
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.
Backports the v6 fix from #42549 to the 5.3.x line. Related to #42443.
---
.bundlewatch.config.json | 8 ++++----
js/src/util/sanitizer.js | 12 ++++++++++--
js/tests/unit/util/sanitizer.spec.js | 8 +++++++-
3 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json
index 6f680664ca67..1d176fe3ee87 100644
--- a/.bundlewatch.config.json
+++ b/.bundlewatch.config.json
@@ -34,7 +34,7 @@
},
{
"path": "./dist/js/bootstrap.bundle.js",
- "maxSize": "43.0 kB"
+ "maxSize": "43.5 kB"
},
{
"path": "./dist/js/bootstrap.bundle.min.js",
@@ -42,7 +42,7 @@
},
{
"path": "./dist/js/bootstrap.esm.js",
- "maxSize": "28.0 kB"
+ "maxSize": "28.5 kB"
},
{
"path": "./dist/js/bootstrap.esm.min.js",
@@ -50,11 +50,11 @@
},
{
"path": "./dist/js/bootstrap.js",
- "maxSize": "28.75 kB"
+ "maxSize": "29.25 kB"
},
{
"path": "./dist/js/bootstrap.min.js",
- "maxSize": "16.25 kB"
+ "maxSize": "16.5 kB"
}
],
"ci": {
diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js
index bcd565a9cfef..5f29cb2b3e0f 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-z=]+$/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 8c91b305c92035cfd1d58079799fb799561215cc Mon Sep 17 00:00:00 2001
From: Aljo Joby <141745680+aljojoby9@users.noreply.github.com>
Date: Fri, 7 Aug 2026 01:01:55 +0530
Subject: [PATCH 2/3] Sanitizer: allow audio/mpeg in DATA_URL_PATTERN
audio/mp3 is nonstandard. Accept the real MIME type audio/mpeg so safe
base64 MP3 data URLs keep working after the data: scheme is blocked.
---
js/src/util/sanitizer.js | 2 +-
js/tests/unit/util/sanitizer.spec.js | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js
index 5f29cb2b3e0f..9ce7d9d557e3 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-z=]+$/i
+const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|mpeg|oga|ogg|opus));base64,[\d+/a-z=]+$/i
const allowedAttribute = (attribute, allowedAttributeList) => {
const attributeName = attribute.nodeName.toLowerCase()
diff --git a/js/tests/unit/util/sanitizer.spec.js b/js/tests/unit/util/sanitizer.spec.js
index ccf5c5cf6dd4..0f555d5f6a09 100644
--- a/js/tests/unit/util/sanitizer.spec.js
+++ b/js/tests/unit/util/sanitizer.spec.js
@@ -31,6 +31,7 @@ describe('Sanitizer', () => {
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', // Truncated.
'data:video/webm;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
+ 'data:audio/mpeg;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
'unknown-scheme:abc'
]
From b7be4937d6f5619fe6242a997f8aecbf7a7ec68c Mon Sep 17 00:00:00 2001
From: Aljo Joby <141745680+aljojoby9@users.noreply.github.com>
Date: Tue, 11 Aug 2026 06:27:20 +0530
Subject: [PATCH 3/3] Document base64-only data URLs and tighten bundlewatch
budgets
Call out that DATA_URL_PATTERN rejects non-base64 media payloads, and
trim the JS size caps closer to the actual sanitizer size delta.
---
.bundlewatch.config.json | 6 +++---
js/src/util/sanitizer.js | 5 +++--
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json
index 1d176fe3ee87..0064412d08e4 100644
--- a/.bundlewatch.config.json
+++ b/.bundlewatch.config.json
@@ -34,7 +34,7 @@
},
{
"path": "./dist/js/bootstrap.bundle.js",
- "maxSize": "43.5 kB"
+ "maxSize": "43.25 kB"
},
{
"path": "./dist/js/bootstrap.bundle.min.js",
@@ -42,7 +42,7 @@
},
{
"path": "./dist/js/bootstrap.esm.js",
- "maxSize": "28.5 kB"
+ "maxSize": "28.25 kB"
},
{
"path": "./dist/js/bootstrap.esm.min.js",
@@ -50,7 +50,7 @@
},
{
"path": "./dist/js/bootstrap.js",
- "maxSize": "29.25 kB"
+ "maxSize": "29.0 kB"
},
{
"path": "./dist/js/bootstrap.min.js",
diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js
index 9ce7d9d557e3..21a430d28285 100644
--- a/js/src/util/sanitizer.js
+++ b/js/src/util/sanitizer.js
@@ -66,8 +66,9 @@ const uriAttributes = new Set([
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.
+ * A pattern that matches safe data URLs. Only matches base64-encoded image,
+ * video and audio types — notably NOT `data:text/html`, and not non-base64
+ * media payloads (e.g. `data:image/png,...` without `;base64,`).
*
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L49
*/