fix(core): sanitize translated attribute bindings with interpolations

Fixes that we weren't sanitizing attribute bindings with interpolations if they're marked for translation, for example: `<a href="{{evilLink}}" i18n-href></a>`.

Also adds a bit more test coverage for our sanitization.
This commit is contained in:
Kristiyan Kostadinov
2026-03-10 11:24:07 +01:00
committed by Matthew Beck (Berry)
parent a55475992b
commit b89b0a83a4
2 changed files with 74 additions and 13 deletions
+2 -6
View File
@@ -388,7 +388,7 @@ export function i18nAttributesFirstPass(tView: TView, index: number, values: str
previousElementIndex,
attrName,
countBindings(updateOpCodes),
null,
URI_ATTRS[attrName.toLowerCase()] ? _sanitizeUrl : null,
);
}
}
@@ -810,18 +810,14 @@ function walkIcuTree(
const hasBinding = !!attr.value.match(BINDING_REGEXP);
if (hasBinding) {
if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) {
if (URI_ATTRS[lowerAttrName]) {
generateBindingUpdateOpCodes(
update,
attr.value,
newIndex,
attr.name,
0,
_sanitizeUrl,
URI_ATTRS[lowerAttrName] ? _sanitizeUrl : null,
);
} else {
generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name, 0, null);
}
} else {
ngDevMode &&
console.warn(
@@ -3605,6 +3605,71 @@ describe('runtime i18n', () => {
'translatedText value',
);
});
describe('attribute sanitization', () => {
@Component({template: ''})
class SanitizeAppComp {
url = 'javascript:alert("oh no")';
count = 0;
}
it('should sanitize translated attribute binding', () => {
const fixture = initWithTemplate(SanitizeAppComp, '<a [attr.href]="url" i18n-href></a>');
const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a');
expect(link.getAttribute('href')).toMatch(/^unsafe:/);
});
it('should sanitize translated property binding', () => {
const fixture = initWithTemplate(SanitizeAppComp, '<a [href]="url" i18n-href></a>');
const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a');
expect(link.getAttribute('href')).toMatch(/^unsafe:/);
});
it('should sanitize translated interpolation', () => {
const fixture = initWithTemplate(SanitizeAppComp, '<a href="{{url}}" i18n-href></a>');
const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a');
expect(link.getAttribute('href')).toMatch(/^unsafe:/);
});
it('should sanitize interpolation inside translated element', () => {
const fixture = initWithTemplate(SanitizeAppComp, `<div i18n><a href="{{url}}"></a></div>`);
const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a');
expect(link.getAttribute('href')).toMatch(/^unsafe:/);
});
it('should sanitize attribute binding inside translated element', () => {
const fixture = initWithTemplate(
SanitizeAppComp,
`<div i18n><a [attr.href]="url"></a></div>`,
);
const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a');
expect(link.getAttribute('href')).toMatch(/^unsafe:/);
});
it('should sanitize property binding inside translated element', () => {
const fixture = initWithTemplate(SanitizeAppComp, `<div i18n><a [href]="url"></a></div>`);
const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a');
expect(link.getAttribute('href')).toMatch(/^unsafe:/);
});
it('should sanitize property binding inside an ICU', () => {
const fixture = initWithTemplate(
SanitizeAppComp,
`<div i18n>{count, plural,
=0 {no <strong>link</strong> yet}
other {{{count}} Here is the <a href="{{url}}">link</a>!}
}</div>`,
);
expect(fixture.nativeElement.querySelector('a')).toBeFalsy();
fixture.componentInstance.count = 1;
fixture.detectChanges();
const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a');
expect(link).toBeTruthy();
expect(link.getAttribute('href')).toMatch(/^unsafe:/);
});
});
});
function initWithTemplate(compType: Type<any>, template: string) {