refactor(compiler): remove unnecessary sanitization for safe attributes

Remove sanitization for attributes that cannot execute code (e.g. `javascript: URIs`).

(cherry picked from commit 128aef0ede)
This commit is contained in:
Alan Agius 2025-12-12 17:05:58 +01:00 committed by Andrew Kushnir
parent 6d9d2b83f6
commit b8cb6cda30
12 changed files with 29 additions and 60 deletions

View file

@ -905,10 +905,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
export class HostBindingImageDir {
constructor() {
this.evil = 'evil';
this.nonEvil = 'nonEvil';
}
}
HostBindingImageDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingImageDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
HostBindingImageDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingImageDir, isStandalone: true, selector: "img[hostBindingImgDir]", host: { properties: { "innerHtml": "evil", "attr.style": "evil", "src": "evil" } }, ngImport: i0 });
HostBindingImageDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingImageDir, isStandalone: true, selector: "img[hostBindingImgDir]", host: { properties: { "innerHtml": "evil", "attr.style": "evil", "src": "nonEvil" } }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingImageDir, decorators: [{
type: Directive,
args: [{
@ -916,7 +917,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
host: {
'[innerHtml]': 'evil',
'[attr.style]': 'evil',
'[src]': 'evil',
'[src]': 'nonEvil',
},
}]
}] });
@ -969,6 +970,7 @@ export declare class HostBindingLinkDir {
}
export declare class HostBindingImageDir {
evil: string;
nonEvil: string;
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingImageDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingImageDir, "img[hostBindingImgDir]", never, {}, {}, never, never, true, never>;
}

View file

@ -7,14 +7,14 @@ hostBindings: function HostBindingLinkDir_HostBindings(rf, ctx) {
hostBindings: function HostBindingImageDir_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("src", ctx.evil, $r3$.ɵɵsanitizeUrl);
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("src", ctx.nonEvil);
$r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle);
}
}
hostBindings: function HostBindingIframeDir_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("src", ctx.evil, $r3$.ɵɵsanitizeResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateAttribute);
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("src", ctx.evil, i0.ɵɵsanitizeResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateAttribute);
$r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle)("attributeName", ctx.nonEvil);
}
}

View file

@ -17,11 +17,12 @@ export class HostBindingLinkDir {
host: {
'[innerHtml]': 'evil',
'[attr.style]': 'evil',
'[src]': 'evil',
'[src]': 'nonEvil',
},
})
export class HostBindingImageDir {
evil = 'evil';
nonEvil = 'nonEvil';
}
@Directive({

View file

@ -827,6 +827,7 @@ import * as i0 from "@angular/core";
export class MyComponent {
constructor() {
this.evil = 'evil';
this.nonEvil = 'nonEvil';
}
}
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
@ -834,7 +835,7 @@ MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "
<div [innerHtml]="evil"></div>
<link [href]="evil" />
<div [attr.style]="evil"></div>
<img [src]="evil" />
<img [src]="nonEvil" />
<iframe [sandbox]="evil"></iframe>
<a href="{{evil}}{{evil}}"></a>
<div attr.style="{{evil}}{{evil}}"></div>
@ -847,7 +848,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
<div [innerHtml]="evil"></div>
<link [href]="evil" />
<div [attr.style]="evil"></div>
<img [src]="evil" />
<img [src]="nonEvil" />
<iframe [sandbox]="evil"></iframe>
<a href="{{evil}}{{evil}}"></a>
<div attr.style="{{evil}}{{evil}}"></div>
@ -861,6 +862,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
import * as i0 from "@angular/core";
export declare class MyComponent {
evil: string;
nonEvil: string;
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}

View file

@ -9,7 +9,7 @@ template: function MyComponent_Template(rf, ctx) {
$r3$.ɵɵadvance();
$r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle);
$r3$.ɵɵadvance();
$r3$.ɵɵdomProperty("src", ctx.evil, $r3$.ɵɵsanitizeUrl);
$r3$.ɵɵdomProperty("src", ctx.nonEvil);
$r3$.ɵɵadvance();
$r3$.ɵɵdomProperty("sandbox", ctx.evil, $r3$.ɵɵvalidateAttribute);
$r3$.ɵɵadvance();

View file

@ -6,7 +6,7 @@ import {Component} from '@angular/core';
<div [innerHtml]="evil"></div>
<link [href]="evil" />
<div [attr.style]="evil"></div>
<img [src]="evil" />
<img [src]="nonEvil" />
<iframe [sandbox]="evil"></iframe>
<a href="{{evil}}{{evil}}"></a>
<div attr.style="{{evil}}{{evil}}"></div>
@ -14,4 +14,5 @@ import {Component} from '@angular/core';
})
export class MyComponent {
evil = 'evil';
nonEvil = 'nonEvil';
}

View file

@ -8497,7 +8497,7 @@ runInEachFileSystem((os: string) => {
hostVars: 6,
hostBindings: function UnsafeAttrsDirective_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.attrAction, i0.ɵɵsanitizeUrl)("profile", ctx.attrProfile, i0.ɵɵsanitizeResourceUrl)("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.attrSafeTitle);
i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.attrSrc, i0.ɵɵsanitizeResourceUrl)("action", ctx.attrAction, i0.ɵɵsanitizeUrl)("profile", ctx.attrProfile)("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.attrSafeTitle);
}
}
`;

View file

@ -32,23 +32,9 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} {
registerContext(SecurityContext.URL, [
'*|formAction',
'area|href',
'area|ping',
'audio|src',
'a|href',
'a|xlink:href',
'a|ping',
'blockquote|cite',
'body|background',
'del|cite',
'form|action',
'img|src',
'input|src',
'ins|cite',
'q|cite',
'source|src',
'track|src',
'video|poster',
'video|src',
// MathML namespace
// https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1
@ -121,16 +107,11 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} {
]);
registerContext(SecurityContext.RESOURCE_URL, [
'applet|code',
'applet|codebase',
'base|href',
'embed|src',
'frame|src',
'head|profile',
'html|manifest',
'iframe|src',
'link|href',
'media|src',
'object|codebase',
'object|data',
'script|src',

View file

@ -156,7 +156,6 @@ If 'onAnything' is a directive input, make sure the directive is imported by the
expect(registry.securityContext('p', 'innerHTML', false)).toBe(SecurityContext.HTML);
expect(registry.securityContext('a', 'href', false)).toBe(SecurityContext.URL);
expect(registry.securityContext('a', 'style', false)).toBe(SecurityContext.STYLE);
expect(registry.securityContext('ins', 'cite', false)).toBe(SecurityContext.URL);
expect(registry.securityContext('base', 'href', false)).toBe(SecurityContext.RESOURCE_URL);
});

View file

@ -1607,23 +1607,6 @@ describe('host bindings', () => {
true,
true,
);
verify(
'blockquote',
'cite',
'javascript:alert(2)',
'unsafe:javascript:alert(2)',
bypassSanitizationTrustUrl,
);
verify('blockquote', 'cite', 'javascript:alert(2.1)', 'unsafe:javascript:alert(2.1)', identity);
verify(
'blockquote',
'cite',
'javascript:alert(2.2)',
'unsafe:javascript:alert(2.2)',
bypassSanitizationTrustHtml,
true,
true,
);
verify(
'b',
'innerHTML',

View file

@ -116,7 +116,7 @@ describe('property interpolation', () => {
it('should not allow unsanitary urls in interpolated properties', () => {
@Component({
template: `
<img src="{{naughty}}">
<a href="{{naughty}}">text</a>
`,
standalone: false,
})
@ -127,15 +127,15 @@ describe('property interpolation', () => {
TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const img: HTMLImageElement = fixture.nativeElement.querySelector('img');
const a = fixture.nativeElement.querySelector('a');
expect(img.src.indexOf('unsafe:')).toBe(0);
expect(a.href.indexOf('unsafe:')).toBe(0);
});
it('should not allow unsanitary urls in interpolated properties, even if you are tricky', () => {
@Component({
template: `
<img src="{{ja}}{{va}}script:{{naughty}}">
<a href="{{ja}}{{va}}script:{{naughty}}">text</a>
`,
standalone: false,
})
@ -148,9 +148,9 @@ describe('property interpolation', () => {
TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const img = fixture.nativeElement.querySelector('img');
const a = fixture.nativeElement.querySelector('a');
expect(img.src.indexOf('unsafe:')).toBe(0);
expect(a.href.indexOf('unsafe:')).toBe(0);
});
it('should handle interpolations with 10+ values', () => {

View file

@ -612,7 +612,7 @@ describe('sanitization', () => {
selector: '[unsafeUrlHostBindingDir]',
})
class UnsafeUrlHostBindingDir {
@HostBinding() cite: any = 'http://cite-dir-value';
@HostBinding() href: any = 'http://href-dir-value';
constructor() {
hostBindingDir = this;
@ -623,7 +623,7 @@ describe('sanitization', () => {
selector: 'sanitize-this',
imports: [UnsafeUrlHostBindingDir],
template: `
<blockquote unsafeUrlHostBindingDir></blockquote>
<a unsafeUrlHostBindingDir>text</a>
`,
})
class SimpleComp {}
@ -640,16 +640,16 @@ describe('sanitization', () => {
],
});
const fixture = TestBed.createComponent(SimpleComp);
hostBindingDir!.cite = 'http://foo';
hostBindingDir!.href = 'http://foo';
fixture.detectChanges();
const anchor = fixture.nativeElement.querySelector('blockquote')!;
expect(anchor.getAttribute('cite')).toEqual('http://bar');
const anchor = fixture.nativeElement.querySelector('a')!;
expect(anchor.getAttribute('href')).toEqual('http://bar');
hostBindingDir!.cite = sanitizer.bypassSecurityTrustUrl('http://foo');
hostBindingDir!.href = sanitizer.bypassSecurityTrustUrl('http://foo');
fixture.detectChanges();
expect(anchor.getAttribute('cite')).toEqual('http://foo');
expect(anchor.getAttribute('href')).toEqual('http://foo');
});
});