fix(core): sanitize host bindings on concrete hosts - #69558
Conversation
027cd32 to
b527d55
Compare
JeanMeche
left a comment
There was a problem hiding this comment.
I took a deeper look at the implementation and identified two edge-case issues related to how security contexts are merged and resolved for dynamic/generic hosts:
1. Over-sanitization of benign custom properties on generic hosts
Because calcHostBindingSecurityContexts merges contexts across all possible DOM elements when a directive's selector doesn't specify a concrete tag, a property like [attr.data] (which is a RESOURCE_URL on <object> but just NONE on a generic <div>) results in a context array of [SecurityContext.RESOURCE_URL, SecurityContext.NONE].
The isUrlOrResourceUrlSecurityContext function correctly evaluates this to true and instructs the compiler to emit the ɵɵsanitizeUrlOrResourceUrl runtime sanitizer.
However, at runtime, if the concrete element happens to be a <div>, getUrlSanitizer in sanitization.ts falls back to ɵɵsanitizeUrl (because div is not in the RESOURCE_MAP for data). As a result, a completely safe string bound to a custom [attr.data] on a <div> (e.g., 'javascript:something') will be unexpectedly over-sanitized and prefixed with unsafe:, mutating benign data attributes.
2. Inconsistent fallback handling for mixed non-URL contexts
In calcHostBindingSecurityContexts, the fallback logic drops the SecurityContext.NONE context if other contexts are present:
if (
(hasConcreteHostNoneContext && concreteHostNonNoneCount > 0) ||
// ...
) {
return concreteHostNonNoneContexts;
}If a property requires a completely different sanitizer (like [attr.srcdoc] which maps to SecurityContext.HTML on an iframe and NONE elsewhere), the compiler drops NONE and correctly hardcodes ɵɵsanitizeHtml for the binding. While this works safely for srcdoc, it silently ignores the fact that the property is supposed to be unsanitized (NONE) on other tags, leading to unconditional HTML sanitization across all tags.
Additionally, if a custom schema or future DOM property mapped an attribute to [SecurityContext.RESOURCE_URL, SecurityContext.SCRIPT], isUrlOrResourceUrlSecurityContext would evaluate to false and cause getOnlySecurityContext() to throw a compilation error, as it doesn't gracefully handle mixed contexts outside of the hardcoded URL/RESOURCE_URL scenario.
Good catch, in that case I think we possibly need to generate a new instruction to sanitize host bindings, since I tried to make the fewest possible changes, which is the solution I proposed. But I agree this is an edge case that we have to consider. Although I also think it would increase the bundle size. It seems we could use If we agree, we could review it, although in my experimentation I found that this increases the bundle generated specifically in the router when bringing in specific elements from the sanitization schema through the use of |
b527d55 to
3936609
Compare
|
|
||
| expect(() => ɵɵsanitizeUrlOrResourceUrl('http://server', 'iframe', 'SRC')).toThrowError(ERROR); | ||
|
|
||
| expect(ɵɵsanitizeUrlOrResourceUrl('javascript:true', 'ScRiPt', 'xLiNk:HrEf')).toEqual( |
There was a problem hiding this comment.
I'm removing the script case since it's not possible to write a script in any way with the latest fixes.
See
GHSA-692r-grfm-v8x7
#69551
|
I updated The concrete cases are now handled like this:
I also changed the URL runtime helper to return For truly ambiguous mixed non-URL contexts, like a hypothetical |
There was a problem hiding this comment.
Nice work here! The logic looks good, but I’d like to spend a bit more time reviewing the unit tests to see if we can make them easier to follow, I can be just me, but I personally am having quite a hard time to follow them.
I also noticed quite a bit of redundant code in sanitization.ts. To save some time from going back and forth, I'll push a quick commit to your branch to clean that up.
| } | ||
|
|
||
| function namespaceUriToKey(namespaceUri: string | null | undefined): string | null { | ||
| switch (namespaceUri?.toLowerCase()) { |
There was a problem hiding this comment.
I fixed this in my commit, but for visibility, the toLowerCase here is incorrect. Namespaces URIs are case sensitive.
c4895b9 to
d119c78
Compare
alan-agius4
left a comment
There was a problem hiding this comment.
LGTM
Reviewed-for: fw-security
josephperrott
left a comment
There was a problem hiding this comment.
LGTM
Reviewed-for: fw-security
7a13039 to
805b371
Compare
|
@JeanMeche & @alan-agius4 I think it's ready this PR, or would we need something else before adding it to the merge queue? |
805b371 to
cddd521
Compare
Host binding sanitization previously used the declaring directive or component selector to choose a compile-time security context. The same host binding can execute on a different concrete element through hostDirectives, inherited host bindings, dynamic directives, or createComponent hostElement usage. Compute host binding security contexts against possible concrete hosts and defer URL versus ResourceURL selection to runtime when necessary. Resolve dynamic root host TNodes to their native tag before sanitizer and security-sensitive attribute checks. Fixes angular#69550
Make runtime URL sanitizer selection namespace-aware so SVG and MathML host bindings match the security schema. Cover SVG href/xlink:href and MathML href host binding cases, including dynamic hostElement resolution.
cddd521 to
777c258
Compare
Make runtime URL sanitizer selection namespace-aware so SVG and MathML host bindings match the security schema. Cover SVG href/xlink:href and MathML href host binding cases, including dynamic hostElement resolution. PR Close #69558
Host binding sanitization previously used the declaring directive or component selector to choose a compile-time security context. The same host binding can execute on a different concrete element through hostDirectives, inherited host bindings, dynamic directives, or createComponent hostElement usage. Compute host binding security contexts against possible concrete hosts and defer URL versus ResourceURL selection to runtime when necessary. Resolve dynamic root host TNodes to their native tag before sanitizer and security-sensitive attribute checks. Fixes #69550 PR Close #69558
Make runtime URL sanitizer selection namespace-aware so SVG and MathML host bindings match the security schema. Cover SVG href/xlink:href and MathML href host binding cases, including dynamic hostElement resolution. PR Close #69558
Host binding sanitization previously used the declaring directive or component selector to choose a compile-time security context. The same host binding can execute on a different concrete element through hostDirectives, inherited host bindings, dynamic directives, or createComponent hostElement usage. Compute host binding security contexts against possible concrete hosts and defer URL versus ResourceURL selection to runtime when necessary. Resolve dynamic root host TNodes to their native tag before sanitizer and security-sensitive attribute checks. Fixes #69550 PR Close #69558
Make runtime URL sanitizer selection namespace-aware so SVG and MathML host bindings match the security schema. Cover SVG href/xlink:href and MathML href host binding cases, including dynamic hostElement resolution. PR Close #69558
Host binding sanitization previously used the declaring directive or component selector to choose a compile-time security context. The same host binding can execute on a different concrete element through hostDirectives, inherited host bindings, dynamic directives, or createComponent hostElement usage.
Compute host binding security contexts against possible concrete hosts and defer URL versus ResourceURL selection to runtime when necessary. Resolve dynamic root host TNodes to their native tag before sanitizer and security-sensitive attribute checks.
Fixes #69550
More context https://issuetracker.google.com/u/1/issues/513926480