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