diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 6f680664ca67..0064412d08e4 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.25 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js", @@ -42,7 +42,7 @@ }, { "path": "./dist/js/bootstrap.esm.js", - "maxSize": "28.0 kB" + "maxSize": "28.25 kB" }, { "path": "./dist/js/bootstrap.esm.min.js", @@ -50,11 +50,11 @@ }, { "path": "./dist/js/bootstrap.js", - "maxSize": "28.75 kB" + "maxSize": "29.0 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..21a430d28285 100644 --- a/js/src/util/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -63,14 +63,23 @@ 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 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 + */ +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() 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..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' ] @@ -67,7 +68,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 = [ '