From 2b5d59e6372b8e3d132583e4d294267f9c24295f Mon Sep 17 00:00:00 2001 From: Hexix23 Date: Mon, 4 May 2026 23:41:02 +0200 Subject: [PATCH] fix(compiler): disallow i18n on svg animation attributes Reject translated SVG animation attributes so localization cannot retarget safe animations to href values. --- .../compiler/src/render3/view/i18n/meta.ts | 30 ++++++++++++++++--- .../test/linker/security_integration_spec.ts | 22 ++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/render3/view/i18n/meta.ts b/packages/compiler/src/render3/view/i18n/meta.ts index f5071b3f8fc5..b5aebace4fd4 100644 --- a/packages/compiler/src/render3/view/i18n/meta.ts +++ b/packages/compiler/src/render3/view/i18n/meta.ts @@ -12,7 +12,10 @@ import * as i18n from '../../../i18n/i18n_ast'; import {createI18nMessageFactory, VisitNodeFn} from '../../../i18n/i18n_parser'; import * as html from '../../../ml_parser/ast'; import {ParseTreeResult} from '../../../ml_parser/parser'; +import {splitNsName} from '../../../ml_parser/tags'; import * as o from '../../../output/output_ast'; +import {SecurityContext} from '../../../core'; +import {DomElementSchemaRegistry} from '../../../schema/dom_element_schema_registry'; import {isTrustedTypesSink} from '../../../schema/trusted_types_sinks'; import {hasI18nAttrs, I18N_ATTR, I18N_ATTR_PREFIX, icuFromI18nMessage} from './util'; @@ -49,6 +52,19 @@ const setI18nRefs = (originalNodeMap: Map): VisitNodeFn => }; }; +const domSchema = new DomElementSchemaRegistry(); +const SVG_ANIMATION_ELEMENTS = new Set(['animate', 'set', 'animatemotion', 'animatetransform']); + +function isSvgAnimationTranslatedAttribute(tagName: string, attrName: string): boolean { + const elementName = splitNsName(tagName)[1]; + + return ( + SVG_ANIMATION_ELEMENTS.has(elementName.toLowerCase()) && + domSchema.securityContext(elementName, attrName, /* isAttribute */ true) === + SecurityContext.ATTRIBUTE_NO_BINDING + ); +} + /** * This visitor walks over HTML parse tree and converts information stored in * i18n-related attributes ("i18n" and "i18n-*") into i18n meta object that is @@ -201,14 +217,20 @@ export class I18nMetaVisitor implements html.Visitor { } else if (attr.name.startsWith(I18N_ATTR_PREFIX)) { // 'i18n-*' attributes const name = attr.name.slice(I18N_ATTR_PREFIX.length); - let isTrustedType: boolean; + let isSecuritySensitive: boolean; if (node instanceof html.Component) { - isTrustedType = node.tagName === null ? false : isTrustedTypesSink(node.tagName, name); + isSecuritySensitive = + node.tagName === null + ? false + : isTrustedTypesSink(node.tagName, name) || + isSvgAnimationTranslatedAttribute(node.tagName, name); } else { - isTrustedType = isTrustedTypesSink(node.name, name); + isSecuritySensitive = + isTrustedTypesSink(node.name, name) || + isSvgAnimationTranslatedAttribute(node.name, name); } - if (isTrustedType) { + if (isSecuritySensitive) { this._reportError( attr, `Translating attribute '${name}' is disallowed for security reasons.`, diff --git a/packages/core/test/linker/security_integration_spec.ts b/packages/core/test/linker/security_integration_spec.ts index 860ab92d9b36..59d5eaa8be5b 100644 --- a/packages/core/test/linker/security_integration_spec.ts +++ b/packages/core/test/linker/security_integration_spec.ts @@ -273,5 +273,27 @@ describe('security integration tests', function () { /Translating attribute 'innerHTML' is disallowed for security reasons./, ); }); + + it('should throw error on SVG animation retargeting attributes', () => { + const template = ` + + + + + + + `; + TestBed.overrideComponent(SecuredComponent, {set: {template}}); + + expect(() => TestBed.createComponent(SecuredComponent)).toThrowError( + /Translating attribute 'attributeName' is disallowed for security reasons./, + ); + }); }); });