diff --git a/CHANGELOG.md b/CHANGELOG.md index d3145bf7c4ab..b0a3b6e3cb27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ + + +# 20.3.15 (2025-12-01) + +### compiler + +| Commit | Type | Description | +| ------------------------------------------------------------------------------------------------ | ---- | ----------------------------------------------------------------- | +| [d1ca8ae043](https://github.com/angular/angular/commit/d1ca8ae04390f050039fdb653a6147d75d48f81e) | fix | prevent XSS via SVG animation `attributeName` and MathML/SVG URLs | + + + # 20.3.14 (2025-11-25) diff --git a/goldens/public-api/core/errors.api.md b/goldens/public-api/core/errors.api.md index 253479c96534..c24ac00d6e04 100644 --- a/goldens/public-api/core/errors.api.md +++ b/goldens/public-api/core/errors.api.md @@ -182,6 +182,8 @@ export const enum RuntimeErrorCode { // (undocumented) UNKNOWN_ELEMENT = 304, // (undocumented) + UNSAFE_ATTRIBUTE_BINDING = -910, + // @deprecated (undocumented) UNSAFE_IFRAME_ATTRS = -910, // (undocumented) UNSAFE_VALUE_IN_RESOURCE_URL = 904, diff --git a/package.json b/package.json index f9e0f10ccdb1..7e930b40a332 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "20.3.14", + "version": "20.3.15", "private": true, "description": "Angular - a web framework for modern web apps", "homepage": "https://github.com/angular/angular", diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/iframe_attrs.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/iframe_attrs.js index 42d1bdb67035..e73053fe6f35 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/iframe_attrs.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/iframe_attrs.js @@ -3,6 +3,6 @@ template: function MyComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelement(0, "iframe", 0); } if (rf & 2) { - i0.ɵɵattribute("fetchpriority", "low", i0.ɵɵvalidateIframeAttribute)("allowfullscreen", ctx.fullscreen, i0.ɵɵvalidateIframeAttribute); + i0.ɵɵattribute("fetchpriority", "low", i0.ɵɵvalidateAttribute)("allowfullscreen", ctx.fullscreen, i0.ɵɵvalidateAttribute); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js index b4805ea15ef0..e3affcb7686c 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js @@ -881,7 +881,7 @@ export class HostBindingDir { } } HostBindingDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, deps: [], target: i0.ɵɵFactoryTarget.Directive }); -HostBindingDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir, isStandalone: true, selector: "[hostBindingDir]", host: { properties: { "innerHtml": "evil", "href": "evil", "attr.style": "evil", "src": "evil", "sandbox": "evil" } }, ngImport: i0 }); +HostBindingDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir, isStandalone: true, selector: "[hostBindingDir]", host: { properties: { "innerHtml": "evil", "href": "evil", "attr.style": "evil", "src": "evil", "sandbox": "evil", "attr.attributeName": "nonEvil" } }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, decorators: [{ type: Directive, args: [{ @@ -892,12 +892,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE '[attr.style]': 'evil', '[src]': 'evil', '[sandbox]': 'evil', + '[attr.attributeName]': 'nonEvil', }, }] }] }); export class HostBindingDir2 { constructor() { this.evil = 'evil'; + this.nonEvil = 'nonEvil'; } } HostBindingDir2.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir2, deps: [], target: i0.ɵɵFactoryTarget.Directive }); @@ -915,6 +917,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE }, }] }] }); +export class HostBindingSvgAnimateDir { + constructor() { + this.evil = 'evil'; + } +} +HostBindingSvgAnimateDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingSvgAnimateDir, deps: [], target: i0.ɵɵFactoryTarget.Directive }); +HostBindingSvgAnimateDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingSvgAnimateDir, isStandalone: true, selector: "animateMotion[hostBindingSvgAnimateDir]", host: { properties: { "attr.attributeName": "evil" } }, ngImport: i0 }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingSvgAnimateDir, decorators: [{ + type: Directive, + args: [{ + selector: 'animateMotion[hostBindingSvgAnimateDir]', + host: { + '[attr.attributeName]': 'evil', + }, + }] + }] }); /**************************************************************************************************** * PARTIAL FILE: sanitization.d.ts @@ -927,9 +945,15 @@ export declare class HostBindingDir { } export declare class HostBindingDir2 { evil: string; + nonEvil: string; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵdir: i0.ɵɵDirectiveDeclaration; } +export declare class HostBindingSvgAnimateDir { + evil: string; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; +} /**************************************************************************************************** * PARTIAL FILE: security_sensitive_constant_attributes.js diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.js index 8af169be19ab..faf6080edf92 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.js @@ -1,13 +1,19 @@ hostBindings: function HostBindingDir_HostBindings(rf, ctx) { if (rf & 2) { - $r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.evil, $r3$.ɵɵsanitizeUrlOrResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute); - $r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle); + i0.ɵɵdomProperty("innerHTML", ctx.evil, i0.ɵɵsanitizeHtml)("href", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl)("sandbox", ctx.evil, i0.ɵɵvalidateAttribute); + i0.ɵɵattribute("style", ctx.evil, i0.ɵɵsanitizeStyle)("attributeName", ctx.nonEvil, i0.ɵɵvalidateAttribute); } } … hostBindings: function HostBindingDir2_HostBindings(rf, ctx) { if (rf & 2) { - $r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrl)("src", ctx.evil)("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute); - $r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle); + i0.ɵɵdomProperty("innerHTML", ctx.evil, i0.ɵɵsanitizeHtml)("href", ctx.evil, i0.ɵɵsanitizeUrl)("src", ctx.evil)("sandbox", ctx.evil); + i0.ɵɵattribute("style", ctx.evil, i0.ɵɵsanitizeStyle); } } +… +hostBindings: function HostBindingSvgAnimateDir_HostBindings(rf, ctx) { + if (rf & 2) { + i0.ɵɵattribute("attributeName", ctx.evil, i0.ɵɵvalidateAttribute); + } +} \ No newline at end of file diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.ts index 4e63f7287d8c..92b08f33907f 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.ts @@ -8,6 +8,7 @@ import {Directive} from '@angular/core'; '[attr.style]': 'evil', '[src]': 'evil', '[sandbox]': 'evil', + '[attr.attributeName]': 'nonEvil', }, }) export class HostBindingDir { @@ -26,4 +27,15 @@ export class HostBindingDir { }) export class HostBindingDir2 { evil = 'evil'; + nonEvil = 'nonEvil'; +} + +@Directive({ + selector: 'animateMotion[hostBindingSvgAnimateDir]', + host: { + '[attr.attributeName]': 'evil', + }, +}) +export class HostBindingSvgAnimateDir { + evil = 'evil'; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/property_bindings/sanitization.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/property_bindings/sanitization.js index 340001152844..843fe28a9546 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/property_bindings/sanitization.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/property_bindings/sanitization.js @@ -11,7 +11,7 @@ template: function MyComponent_Template(rf, ctx) { $r3$.ɵɵadvance(); $r3$.ɵɵdomProperty("src", ctx.evil, $r3$.ɵɵsanitizeUrl); $r3$.ɵɵadvance(); - $r3$.ɵɵdomProperty("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute); + $r3$.ɵɵdomProperty("sandbox", ctx.evil, $r3$.ɵɵvalidateAttribute); $r3$.ɵɵadvance(); $r3$.ɵɵdomProperty("href", $r3$.ɵɵinterpolate2("", ctx.evil, "", ctx.evil), $r3$.ɵɵsanitizeUrl); $r3$.ɵɵadvance(); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 6937bdb921c1..790cf539368e 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -9261,6 +9261,33 @@ runInEachFileSystem((os: string) => { }); }); + describe('SVG animation processing', () => { + it('should generate SVG animation validation instruction', () => { + env.write( + 'test.ts', + ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: '', + standalone: false, + }) + export class TestCmp { + attr = 'opacity'; + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'i0.ɵɵattribute("attributeName", ctx.attr, i0.ɵɵvalidateAttribute);', + ); + }); + }); + describe('inline resources', () => { it('should process inline