Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions packages/core/src/sanitization/sanitization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ function getSanitizer(): Sanitizer | null {
}

const attributeName: ReadonlySet<string> = new Set(['attributename']);
const SVG_ANIMATION_ATTRIBUTE_NAME_CANDIDATES = ['attributeName', 'attributename'] as const;

/**
* @remarks Keep this in sync with DOM Security Schema.
Expand Down Expand Up @@ -358,9 +359,12 @@ export function ɵɵvalidateAttribute<T = any>(value: T, tagName: string, attrib
}

const element = getNativeByTNode(tNode, lView) as SVGAnimateElement;
const attributeNameValue = element.getAttribute('attributeName');
const attributeNameValue = getSecuritySensitiveSVGAnimationAttributeName(
element,
validationConfig,
);

if (attributeNameValue && validationConfig.has(attributeNameValue.toLowerCase())) {
if (attributeNameValue) {
const errorMessage =
ngDevMode &&
`Angular has detected that the \`${attributeName}\` was applied ` +
Expand All @@ -385,3 +389,17 @@ export function ɵɵvalidateAttribute<T = any>(value: T, tagName: string, attrib
`in a template or in host bindings section.`;
throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage);
}

function getSecuritySensitiveSVGAnimationAttributeName(
element: SVGAnimateElement,
validationConfig: ReadonlySet<string>,
): string | null {
for (const attributeName of SVG_ANIMATION_ATTRIBUTE_NAME_CANDIDATES) {
const attributeNameValue = element.getAttribute(attributeName);
if (attributeNameValue !== null && validationConfig.has(attributeNameValue.toLowerCase())) {
return attributeNameValue;
}
}

return null;
}
24 changes: 24 additions & 0 deletions packages/core/test/render3/integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,30 @@ describe('sanitization', () => {

expect(anchor.getAttribute('href')).toEqual('http://foo');
});

// The SVG `attributeName` is case-sensitive when accessed via the DOM API
// (i.e. `setAttribute('attributename', ...)` and `setAttribute('attributeName', ...)`
// create two distinct attributes). However, the browser tokenizer normalizes
// the lowercase form `attributename` to `attributeName` on initial parsing,
// which means the client-side sanitizer still ends up seeing `attributeName`.
// The SSR renderer (Domino) does not perform this normalization, so we
// explicitly look up the lowercase form as well to make sure the sanitizer
// is triggered consistently in both environments.
it('should throw when binding to set element with attributename="href"', () => {
@Component({
selector: 'test-comp',
template: `<svg><set attributename="href" [attr.to]="'foo'"></set></svg>`,
})
class TestComp {}

TestBed.configureTestingModule({
providers: [provideZoneChangeDetection()],
});
const fixture = TestBed.createComponent(TestComp);
expect(() => fixture.detectChanges()).toThrowError(
/Angular has detected that the `to` was applied/,
);
});
});

class LocalSanitizedValue {
Expand Down
Loading