Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions packages/core/src/sanitization/url_sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,34 @@ import {XSS_SECURITY_URL} from '../error_details_base_url';
* This regular expression was taken from the Closure sanitization library.
*/
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:\/?#]*(?:[\/?#]|$))/i;

/**
* A pattern that matches `data:` URLs with MIME types that can execute scripts
* when navigated to or embedded. These are explicitly blocked to prevent XSS.
*
* Blocked MIME types:
* - `text/html`, `application/xhtml+xml` — rendered as HTML documents with full script execution
* - `text/javascript`, `application/javascript` — executed as JavaScript directly
* - `image/svg+xml` — SVG documents can embed and execute scripts when opened as a document
*
* All other `data:` URLs (e.g. `data:text/csv`, `data:application/pdf`,
* `data:application/json`) remain allowed, preserving common patterns such as
* client-side file generation for download via `<a [href]="..." download>`.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENT: While the intent to block malicious data URLs is good, using a strict allowlist here breaks perfectly safe and common data URLs. For example, data:text/csv or data:application/pdf used in <a download> will now be blocked. A blocklist targeting specific dangerous types (e.g. text/html, application/javascript, image/svg+xml) would prevent the XSS vectors without breaking legitimate client-side file generation.

@SkyZeroZx SkyZeroZx Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, modern browsers already block some of these by default.

If I'm not mistaken, the only possibly valid one would be blob, since it can be used for downloads, but it would be difficult (or impossible) to determine if it's safe or intentional on the part of the developer.

image image image

I also found this:
parallax/jsPDF#2286
https://ourcodeworld.com/articles/read/682/what-does-the-not-allowed-to-navigate-top-frame-to-data-url-javascript-exception-means-in-google-chrome

const UNSAFE_DATA_URL_PATTERN =
/^data:(?:text\/html|application\/xhtml\+xml|text\/javascript|application\/javascript|image\/svg\+xml)[,;]/i;

export function _sanitizeUrl(url: string): string {
url = String(url);
if (url.match(SAFE_URL_PATTERN)) return url;

if (UNSAFE_DATA_URL_PATTERN.test(url)) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
console.warn(`WARNING: sanitizing unsafe URL value ${url} (see ${XSS_SECURITY_URL})`);
}
return 'unsafe:' + url;
}
if (SAFE_URL_PATTERN.test(url)) return url;
if (typeof ngDevMode === 'undefined' || ngDevMode) {
console.warn(`WARNING: sanitizing unsafe URL value ${url} (see ${XSS_SECURITY_URL})`);
}

return 'unsafe:' + url;
}
51 changes: 51 additions & 0 deletions packages/core/test/sanitization/url_sanitizer_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ describe('URL sanitizer', () => {
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', // Truncated.
'data:video/webm;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
// Common download use cases must remain valid
'data:text/csv,a,b,c',
'data:text/csv;charset=utf-8,hello%2Cworld',
'data:application/pdf;base64,JVBERi0xLjQ=',
'data:application/json,{"key":"value"}',
'unknown-scheme:abc',
];
for (const url of validUrls) {
Expand All @@ -71,4 +76,50 @@ describe('URL sanitizer', () => {
it(`valid ${url}`, () => expect(_sanitizeUrl(url)).toMatch(/^unsafe:/));
}
});

describe('dangerous data: URLs are sanitized', () => {
const dangerousDataUrls = [
// HTML content executes scripts when navigated to (Firefox allows data: navigation)
'data:text/html,<h1>Hello</h1>',
'data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==',
'data:text/html;charset=utf-8,<script>alert(1)</script>',
'DATA:TEXT/HTML,xss',
// JavaScript MIME types execute scripts directly
'data:application/javascript,alert(1)',
'data:text/javascript,alert(1)',
// XHTML can embed and execute scripts
'data:application/xhtml+xml,<html><script>alert(1)</script></html>',
// SVG executes scripts when opened as a document via link navigation
'data:image/svg+xml,<svg onload="alert(1)"></svg>',
'data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9ImFsZXJ0KDEpIj48L3N2Zz4=',
];
for (const url of dangerousDataUrls) {
it(`blocks "${url}"`, () => {
expect(_sanitizeUrl(url)).toMatch(/^unsafe:/);
expect(logMsgs.join('\n')).toMatch(/sanitizing unsafe URL value/);
});
}
});

describe('non-executable data: URLs remain valid', () => {
const safeDataUrls = [
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ',
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAA=',
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+',
'data:video/mp4;base64,AAAAIGZ0eXBpc29t',
'data:audio/ogg;base64,T2dnUwACAA==',
// Client-side file download patterns must remain unblocked
'data:text/csv,name,age\nAlice,30',
'data:text/csv;charset=utf-8,hello%2Cworld',
'data:application/pdf;base64,JVBERi0xLjQ=',
'data:application/json,{"key":"value"}',
'data:application/octet-stream;base64,AAAA',
];
for (const url of safeDataUrls) {
it(`allows "${url.substring(0, 50)}..."`, () => {
expect(_sanitizeUrl(url)).toEqual(url);
});
}
});
});
Loading