diff --git a/goldens/public-api/core/errors.md b/goldens/public-api/core/errors.md index 8d8790c4064e9..18967f20b656e 100644 --- a/goldens/public-api/core/errors.md +++ b/goldens/public-api/core/errors.md @@ -101,6 +101,8 @@ export const enum RuntimeErrorCode { // (undocumented) UNKNOWN_ELEMENT = 304, // (undocumented) + UNSAFE_IFRAME_ATTRS = 910, + // (undocumented) UNSAFE_VALUE_IN_RESOURCE_URL = 904, // (undocumented) UNSAFE_VALUE_IN_SCRIPT = 905, diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index ec8b0643a2481..81707e24537c1 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -7506,6 +7506,218 @@ function allTests(os: string) { }); }); + describe('iframe processing', () => { + it('should generate attribute and property bindings with a validator fn when on + \` + }) + export class SomeComponent {} + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // Only `sandbox` has an extra validation fn (since it's security-sensitive), + // the `title` property doesn't have an extra validation fn. + expect(jsContents) + .toContain( + 'ɵɵproperty("sandbox", "", i0.ɵɵvalidateIframeAttribute)("title", "Hi!")'); + + // The `allow` property is also security-sensitive, thus an extra validation fn. + expect(jsContents).toContain('ɵɵattribute("allow", "", i0.ɵɵvalidateIframeAttribute)'); + + // Expect an extra validation function on the `element` instruction for an + \` + }) + export class SomeComponent { + visible = true; + } + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // Expect an extra validation function on the `element` instruction for an + \` + }) + export class SomeComponent {} + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // Make sure that the `sandbox` has an extra validation fn, + // and the check is case-insensitive (since the `setAttribute` DOM API + // is case-insensitive as well). + expect(jsContents).toContain('ɵɵattribute("SANDBOX", "", i0.ɵɵvalidateIframeAttribute)'); + + // Expect an extra validation function on the `element` instruction for an + \` + }) + export class SomeComponent {} + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // Expect an extra validation function on the `element` instruction for an `, + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, securityAttr); + }); + + it(`should error when a security-sensitive attribute is located *after* the \`${srcAttr}\` ` + + `(checking \`${securityAttr}\` as a static attribute, making sure it's ` + + `case-insensitive)`, + () => { + @Component({ + standalone: true, + selector: 'my-comp', + template: ` + + `, + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, securityAttr); + }); + + it(`should error when a security-sensitive attribute is located ` + + `*after* the \`${srcAttr}\` (checking \`${securityAttr}\` as a property binding)`, + () => { + @Component({ + standalone: true, + selector: 'my-comp', + template: ``, + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, securityAttr); + }); + + it(`should error when a security-sensitive attribute is located ` + + `*after* the \`${srcAttr}\` (checking \`${securityAttr}\` as a property binding, ` + + `making sure it's case-insensitive)`, + () => { + @Component({ + standalone: true, + selector: 'my-comp', + template: ` + + `, + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, securityAttr.toUpperCase()); + }); + + it(`should error when a security-sensitive attribute is located ` + + `*after* the \`${srcAttr}\` (checking \`${securityAttr}\` as an attribute binding)`, + () => { + @Component({ + standalone: true, + selector: 'my-comp', + template: ` + + `, + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, securityAttr); + }); + + it(`should error when a security-sensitive attribute is located ` + + `*after* the \`${srcAttr}\` (checking \`${securityAttr}\` as an attribute binding, ` + + `making sure it's case-insensitive)`, + () => { + @Component({ + standalone: true, + selector: 'my-comp', + template: ` + + `, + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, securityAttr.toUpperCase()); + }); + + it(`should work when a security-sensitive attribute is set ` + + `before the \`${srcAttr}\` (checking \`${securityAttr}\`)`, + () => { + @Component({ + standalone: true, + selector: 'my-comp', + template: ``, + }) + class IframeComp { + } + + expectIframeToBeCreated(IframeComp, srcAttr, TEST_IFRAME_URL); + }); + + it(`should error when trying to change a security-sensitive attribute after initial creation ` + + `when the \`${srcAttr}\` is set (checking \`${securityAttr}\`)`, + () => { + @Component({ + standalone: true, + selector: 'my-comp', + template: ` + + `, + }) + class IframeComp { + private sanitizer = inject(DomSanitizer); + src = this.sanitizeFn(TEST_IFRAME_URL); + securityAttr = 'allow-forms'; + + get sanitizeFn() { + return srcAttr === 'src' ? this.sanitizer.bypassSecurityTrustResourceUrl : + this.sanitizer.bypassSecurityTrustHtml; + } + } + + const fixture = expectIframeToBeCreated(IframeComp, srcAttr, TEST_IFRAME_URL); + const component = fixture.componentInstance; + + // Expect to throw if security-sensitive attribute is changed + // after the `src` or `srcdoc` is set. + component.securityAttr = 'allow-modals'; + expect(() => fixture.detectChanges()).toThrowError(getErrorMessageRegexp(securityAttr)); + + // However, changing the `src` or `srcdoc` is allowed. + const newUrl = 'https://angular.io/about?group=Angular'; + component.src = component.sanitizeFn(newUrl); + expect(() => fixture.detectChanges()).not.toThrow(); + expect(fixture.nativeElement.querySelector('iframe')[srcAttr]).toEqual(newUrl); + }); + }); + }); + + it('should error when a directive sets a security-sensitive attribute after setting `src`', + () => { + @Directive({ + standalone: true, + selector: '[dir]', + host: { + 'src': TEST_IFRAME_URL, + 'sandbox': '', + }, + }) + class IframeDir { + } + + @Component({ + standalone: true, + imports: [IframeDir], + selector: 'my-comp', + template: '', + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, 'sandbox'); + }); + + it('should error when a security-sensitive attribute is set after `src` on an `, + }) + class IframeComp { + visible = true; + } + + expectIframeCreationToFail(IframeComp, 'sandbox'); + }); + + it('should error when a security-sensitive attribute is set between `src` and `srcdoc`', () => { + @Component({ + standalone: true, + selector: 'my-comp', + template: ``, + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, 'sandbox'); + }); + + it('should work when a directive sets a security-sensitive attribute before setting `src`', + () => { + @Directive({ + standalone: true, + selector: '[dir]', + host: { + 'sandbox': '', + 'src': TEST_IFRAME_URL, + }, + }) + class IframeDir { + } + + @Component({ + standalone: true, + imports: [IframeDir], + selector: 'my-comp', + template: '', + }) + class IframeComp { + } + + expectIframeToBeCreated(IframeComp, 'src', TEST_IFRAME_URL); + }); + + it('should error when a directive sets an `src` and ' + + 'there was a security-sensitive attribute set in a template' + + '(directive attribute after `sandbox`)', + () => { + @Directive({ + standalone: true, + selector: '[dir]', + host: { + 'src': TEST_IFRAME_URL, + }, + }) + class IframeDir { + } + + @Component({ + standalone: true, + imports: [IframeDir], + selector: 'my-comp', + template: '', + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, 'sandbox'); + }); + + it('should error when a directive sets a security-sensitive attribute in uppercase ' + + 'and it gets applied before an `src` value', + () => { + @Directive({ + standalone: true, + selector: '[dir]', + host: { + '[attr.SANDBOX]': '\'\'', + }, + }) + class IframeDir { + } + + @Component({ + standalone: true, + imports: [IframeDir], + selector: 'my-comp', + template: ``, + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, 'SANDBOX'); + }); + + it('should error when a directive sets an `src` and ' + + 'there was a security-sensitive attribute set in a template' + + '(directive attribute before `sandbox`)', + () => { + @Directive({ + standalone: true, + selector: '[dir]', + host: { + 'src': TEST_IFRAME_URL, + }, + }) + class IframeDir { + } + + @Component({ + standalone: true, + imports: [IframeDir], + selector: 'my-comp', + template: '', + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, 'sandbox'); + }); + + it('should work when a directive sets a security-sensitive attribute and ' + + 'there was an `src` attribute set in a template' + + '(directive attribute after `src`)', + () => { + @Directive({ + standalone: true, + selector: '[dir]', + host: { + 'sandbox': '', + }, + }) + class IframeDir { + } + + @Component({ + standalone: true, + imports: [IframeDir], + selector: 'my-comp', + template: ``, + }) + class IframeComp { + } + + expectIframeToBeCreated(IframeComp, 'src', TEST_IFRAME_URL); + }); + + it('should work when a directive sets a security-sensitive attribute and ' + + 'there was an `src` attribute set in a template' + + '(directive attribute before `src`)', + () => { + @Directive({ + standalone: true, + selector: '[dir]', + host: { + 'sandbox': '', + }, + }) + class IframeDir { + } + + @Component({ + standalone: true, + imports: [IframeDir], + selector: 'my-comp', + template: ``, + }) + class IframeComp { + } + + expectIframeToBeCreated(IframeComp, 'src', TEST_IFRAME_URL); + }); + + it('should error when a directive that sets a security-sensitive attribute goes ' + + 'after the directive that sets an `src` attribute value', + () => { + @Directive({ + standalone: true, + selector: '[set-src]', + host: { + 'src': TEST_IFRAME_URL, + }, + }) + class DirThatSetsSrc { + } + + @Directive({ + standalone: true, + selector: '[set-sandbox]', + host: { + 'sandbox': '', + }, + }) + class DirThatSetsSandbox { + } + + @Component({ + standalone: true, + imports: [DirThatSetsSrc, DirThatSetsSandbox], + selector: 'my-comp', + template: '', + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, 'sandbox'); + }); + + it('should work when a directive that sets a security-sensitive attribute goes ' + + 'before the directive that sets an `src` attribute value', + () => { + @Directive({ + standalone: true, + selector: '[set-src]', + host: { + 'src': TEST_IFRAME_URL, + }, + }) + class DirThatSetsSrc { + } + + @Directive({ + standalone: true, + selector: '[set-sandbox]', + host: { + 'sandbox': '', + }, + }) + class DirThatSetsSandbox { + } + + @Component({ + standalone: true, + imports: [DirThatSetsSandbox, DirThatSetsSrc], + selector: 'my-comp', + // Important note: even though the `set-sandbox` goes after the `set-src`, + // the directive matching order (thus the order of host attributes) is + // based on the imports order, so the `sandbox` gets set first and the `src` second. + template: '', + }) + class IframeComp { + } + + expectIframeToBeCreated(IframeComp, 'src', TEST_IFRAME_URL); + }); + + it(`should error when a security-sensitive attribute is located after the \`src\` ` + + `on an `, + }) + class IframeComp { + } + + expectIframeCreationToFail(IframeComp, 'sandbox'); + }); + + it('should error when a directive that sets a security-sensitive attribute has ' + + 'a host directive that sets an `src` attribute value', + () => { + @Directive({ + standalone: true, + selector: '[set-src-dir]', + host: { + 'src': TEST_IFRAME_URL, + }, + }) + class DirThatSetsSrc { + } + + @Directive({ + standalone: true, + selector: '[dir]', + hostDirectives: [DirThatSetsSrc], + host: { + 'sandbox': '', + }, + }) + class DirThatSetsSandbox { + } + + @Component({ + standalone: true, + imports: [DirThatSetsSandbox], + selector: 'my-comp', + template: '', + }) + class IframeComp { + } + + // Note: host bindings of the `DirThatSetsSrc` (thus setting the `src`) + // were invoked first, since this is a host directive of the `DirThatSetsSandbox` + // (in which case, the `sandbox` is set afterwards, which causes an error). + expectIframeCreationToFail(IframeComp, 'sandbox'); + }); + + it('should work when a directive that sets an `src` has ' + + 'a host directive that sets a security-sensitive attribute value', + () => { + @Directive({ + standalone: true, + selector: '[set-sandbox-dir]', + host: { + 'sandbox': '', + }, + }) + class DirThatSetsSandbox { + } + + @Directive({ + standalone: true, + selector: '[dir]', + hostDirectives: [DirThatSetsSandbox], + host: { + 'src': TEST_IFRAME_URL, + }, + }) + class DirThatSetsSrc { + } + + @Component({ + standalone: true, + imports: [DirThatSetsSrc], + selector: 'my-comp', + template: '', + }) + class IframeComp { + } + + // Note: host bindings of the `DirThatSetsSandbox` (thus setting the `sandbox`) + // were invoked first, since this is a host directive of the `DirThatSetsSrc` + // (in which case, the `src` is set afterwards, which is ok). + expectIframeToBeCreated(IframeComp, 'src', TEST_IFRAME_URL); + }); +});