Skip to content

Commit 00a1b3b

Browse files
mmalerbaalxhub
authored andcommitted
refactor(compiler): Add sanitization support for host bindings (angular#53513)
Adds support for sanitizing host bindings. Since the tag name of the element the host binding is being set on isn't always known, we have to consider multiple possible security contexts. This commit also adds additional tests to help verify correct behavior of the sanitization logic for different edge cases. PR Close angular#53513
1 parent 44f9f01 commit 00a1b3b

File tree

23 files changed

+487
-48
lines changed

23 files changed

+487
-48
lines changed

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,3 +767,144 @@ export declare class MyModule {
767767
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
768768
}
769769

770+
/****************************************************************************************************
771+
* PARTIAL FILE: sanitization.js
772+
****************************************************************************************************/
773+
import { Directive } from '@angular/core';
774+
import * as i0 from "@angular/core";
775+
export class HostBindingDir {
776+
constructor() {
777+
this.evil = 'evil';
778+
}
779+
}
780+
HostBindingDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
781+
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 });
782+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, decorators: [{
783+
type: Directive,
784+
args: [{
785+
standalone: true,
786+
selector: '[hostBindingDir]',
787+
host: {
788+
'[innerHtml]': 'evil',
789+
'[href]': 'evil',
790+
'[attr.style]': 'evil',
791+
'[src]': 'evil',
792+
'[sandbox]': 'evil',
793+
},
794+
}]
795+
}] });
796+
export class HostBindingDir2 {
797+
constructor() {
798+
this.evil = 'evil';
799+
}
800+
}
801+
HostBindingDir2.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir2, deps: [], target: i0.ɵɵFactoryTarget.Directive });
802+
HostBindingDir2.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir2, isStandalone: true, selector: "a", host: { properties: { "innerHtml": "evil", "href": "evil", "attr.style": "evil", "src": "evil", "sandbox": "evil" } }, ngImport: i0 });
803+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir2, decorators: [{
804+
type: Directive,
805+
args: [{
806+
standalone: true,
807+
selector: 'a',
808+
host: {
809+
'[innerHtml]': 'evil',
810+
'[href]': 'evil',
811+
'[attr.style]': 'evil',
812+
'[src]': 'evil',
813+
'[sandbox]': 'evil',
814+
},
815+
}]
816+
}] });
817+
818+
/****************************************************************************************************
819+
* PARTIAL FILE: sanitization.d.ts
820+
****************************************************************************************************/
821+
import * as i0 from "@angular/core";
822+
export declare class HostBindingDir {
823+
evil: string;
824+
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingDir, never>;
825+
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingDir, "[hostBindingDir]", never, {}, {}, never, never, true, never>;
826+
}
827+
export declare class HostBindingDir2 {
828+
evil: string;
829+
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingDir2, never>;
830+
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingDir2, "a", never, {}, {}, never, never, true, never>;
831+
}
832+
833+
/****************************************************************************************************
834+
* PARTIAL FILE: security_sensitive_constant_attributes.js
835+
****************************************************************************************************/
836+
import { Directive } from '@angular/core';
837+
import * as i0 from "@angular/core";
838+
export class HostBindingDir {
839+
}
840+
HostBindingDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
841+
HostBindingDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir, isStandalone: true, selector: "[hostBindingDir]", host: { attributes: { "src": "trusted", "srcdoc": "trusted" } }, ngImport: i0 });
842+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, decorators: [{
843+
type: Directive,
844+
args: [{
845+
standalone: true,
846+
selector: '[hostBindingDir]',
847+
host: { 'src': 'trusted', 'srcdoc': 'trusted' },
848+
}]
849+
}] });
850+
export class HostBindingDir2 {
851+
}
852+
HostBindingDir2.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir2, deps: [], target: i0.ɵɵFactoryTarget.Directive });
853+
HostBindingDir2.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir2, isStandalone: true, selector: "img", host: { attributes: { "src": "trusted", "srcdoc": "trusted" } }, ngImport: i0 });
854+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir2, decorators: [{
855+
type: Directive,
856+
args: [{
857+
standalone: true,
858+
selector: 'img',
859+
host: { 'src': 'trusted', 'srcdoc': 'trusted' },
860+
}]
861+
}] });
862+
863+
/****************************************************************************************************
864+
* PARTIAL FILE: security_sensitive_constant_attributes.d.ts
865+
****************************************************************************************************/
866+
import * as i0 from "@angular/core";
867+
export declare class HostBindingDir {
868+
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingDir, never>;
869+
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingDir, "[hostBindingDir]", never, {}, {}, never, never, true, never>;
870+
}
871+
export declare class HostBindingDir2 {
872+
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingDir2, never>;
873+
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingDir2, "img", never, {}, {}, never, never, true, never>;
874+
}
875+
876+
/****************************************************************************************************
877+
* PARTIAL FILE: security_sensitive_style_bindings.js
878+
****************************************************************************************************/
879+
import { Directive } from '@angular/core';
880+
import * as i0 from "@angular/core";
881+
export class HostBindingDir {
882+
constructor() {
883+
this.imgUrl = 'url(foo.jpg)';
884+
this.styles = { backgroundImage: this.imgUrl };
885+
}
886+
}
887+
HostBindingDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
888+
HostBindingDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir, isStandalone: true, selector: "[hostBindingDir]", host: { properties: { "style.background-image": "imgUrl", "style": "styles" } }, ngImport: i0 });
889+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, decorators: [{
890+
type: Directive,
891+
args: [{
892+
standalone: true,
893+
selector: '[hostBindingDir]',
894+
host: { '[style.background-image]': 'imgUrl', '[style]': 'styles' },
895+
}]
896+
}] });
897+
898+
/****************************************************************************************************
899+
* PARTIAL FILE: security_sensitive_style_bindings.d.ts
900+
****************************************************************************************************/
901+
import * as i0 from "@angular/core";
902+
export declare class HostBindingDir {
903+
imgUrl: string;
904+
styles: {
905+
backgroundImage: string;
906+
};
907+
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingDir, never>;
908+
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingDir, "[hostBindingDir]", never, {}, {}, never, never, true, never>;
909+
}
910+

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/TEST_CASES.json

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,48 @@
280280
]
281281
}
282282
]
283+
},
284+
{
285+
"description": "should sanitize dangerous bindings",
286+
"inputFiles": [
287+
"sanitization.ts"
288+
],
289+
"expectations": [
290+
{
291+
"failureMessage": "Incorrect template",
292+
"files": [
293+
"sanitization.js"
294+
]
295+
}
296+
]
297+
},
298+
{
299+
"description": "should handle security-sensitive constant attributes",
300+
"inputFiles": [
301+
"security_sensitive_constant_attributes.ts"
302+
],
303+
"expectations": [
304+
{
305+
"failureMessage": "Incorrect template",
306+
"files": [
307+
"security_sensitive_constant_attributes.js"
308+
]
309+
}
310+
]
311+
},
312+
{
313+
"description": "should handle security-sensitive style bindings",
314+
"inputFiles": [
315+
"security_sensitive_style_bindings.ts"
316+
],
317+
"expectations": [
318+
{
319+
"failureMessage": "Incorrect template",
320+
"files": [
321+
"security_sensitive_style_bindings.js"
322+
]
323+
}
324+
]
283325
}
284326
]
285-
}
327+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
hostBindings: function HostBindingDir_HostBindings(rf, ctx) {
2+
if (rf & 2) {
3+
i0.ɵɵhostProperty("innerHtml", ctx.evil, i0.ɵɵsanitizeHtml)("href", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl)("sandbox", ctx.evil, i0.ɵɵvalidateIframeAttribute);
4+
i0.ɵɵattribute("style", ctx.evil, i0.ɵɵsanitizeStyle);
5+
}
6+
}
7+
8+
hostBindings: function HostBindingDir2_HostBindings(rf, ctx) {
9+
if (rf & 2) {
10+
i0.ɵɵhostProperty("innerHtml", ctx.evil, i0.ɵɵsanitizeHtml)("href", ctx.evil, i0.ɵɵsanitizeUrl)("src", ctx.evil)("sandbox", ctx.evil, i0.ɵɵvalidateIframeAttribute);
11+
i0.ɵɵattribute("style", ctx.evil, i0.ɵɵsanitizeStyle);
12+
}
13+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {Directive} from '@angular/core';
2+
3+
@Directive({
4+
standalone: true,
5+
selector: '[hostBindingDir]',
6+
host: {
7+
'[innerHtml]': 'evil',
8+
'[href]': 'evil',
9+
'[attr.style]': 'evil',
10+
'[src]': 'evil',
11+
'[sandbox]': 'evil',
12+
},
13+
})
14+
export class HostBindingDir {
15+
evil = 'evil';
16+
}
17+
18+
@Directive({
19+
standalone: true,
20+
selector: 'a',
21+
host: {
22+
'[innerHtml]': 'evil',
23+
'[href]': 'evil',
24+
'[attr.style]': 'evil',
25+
'[src]': 'evil',
26+
'[sandbox]': 'evil',
27+
},
28+
})
29+
export class HostBindingDir2 {
30+
evil = 'evil';
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
hostAttrs: ["src", "trusted", "srcdoc", "trusted"]
2+
3+
hostAttrs: ["src", "trusted", "srcdoc", "trusted"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {Directive} from '@angular/core';
2+
3+
@Directive({
4+
standalone: true,
5+
selector: '[hostBindingDir]',
6+
host: {'src': 'trusted', 'srcdoc': 'trusted'},
7+
})
8+
export class HostBindingDir {
9+
}
10+
11+
@Directive({
12+
standalone: true,
13+
selector: 'img',
14+
host: {'src': 'trusted', 'srcdoc': 'trusted'},
15+
})
16+
export class HostBindingDir2 {
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
hostBindings: function HostBindingDir_HostBindings(rf, ctx) {
2+
if (rf & 2) {
3+
i0.ɵɵstyleMap(ctx.styles);
4+
i0.ɵɵstyleProp("background-image", ctx.imgUrl);
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {Directive} from '@angular/core';
2+
3+
@Directive({
4+
standalone: true,
5+
selector: '[hostBindingDir]',
6+
host: {'[style.background-image]': 'imgUrl', '[style]': 'styles'},
7+
})
8+
export class HostBindingDir {
9+
imgUrl = 'url(foo.jpg)';
10+
styles = {backgroundImage: this.imgUrl};
11+
}

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/property_bindings/GOLDEN_PARTIAL.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,3 +674,50 @@ export declare class MyComponent {
674674
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-cmp", never, {}, {}, never, never, true, never>;
675675
}
676676

677+
/****************************************************************************************************
678+
* PARTIAL FILE: sanitization.js
679+
****************************************************************************************************/
680+
import { Component } from '@angular/core';
681+
import * as i0 from "@angular/core";
682+
export class MyComponent {
683+
constructor() {
684+
this.evil = 'evil';
685+
}
686+
}
687+
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
688+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: `
689+
<div [innerHtml]="evil"></div>
690+
<link [href]="evil" />
691+
<div [attr.style]="evil"></div>
692+
<img [src]="evil" />
693+
<iframe [sandbox]="evil"></iframe>
694+
<a href="{{evil}}{{evil}}"></a>
695+
<div attr.style="{{evil}}{{evil}}"></div>
696+
`, isInline: true });
697+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
698+
type: Component,
699+
args: [{
700+
selector: 'my-component',
701+
standalone: true,
702+
template: `
703+
<div [innerHtml]="evil"></div>
704+
<link [href]="evil" />
705+
<div [attr.style]="evil"></div>
706+
<img [src]="evil" />
707+
<iframe [sandbox]="evil"></iframe>
708+
<a href="{{evil}}{{evil}}"></a>
709+
<div attr.style="{{evil}}{{evil}}"></div>
710+
`
711+
}]
712+
}] });
713+
714+
/****************************************************************************************************
715+
* PARTIAL FILE: sanitization.d.ts
716+
****************************************************************************************************/
717+
import * as i0 from "@angular/core";
718+
export declare class MyComponent {
719+
evil: string;
720+
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
721+
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
722+
}
723+

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/property_bindings/TEST_CASES.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,20 @@
182182
]
183183
}
184184
]
185+
},
186+
{
187+
"description": "should sanitize dangerous bindings",
188+
"inputFiles": [
189+
"sanitization.ts"
190+
],
191+
"expectations": [
192+
{
193+
"failureMessage": "Incorrect template",
194+
"files": [
195+
"sanitization.js"
196+
]
197+
}
198+
]
185199
}
186200
]
187-
}
201+
}

0 commit comments

Comments
 (0)