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/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json index dc858da6640ef..1bd322e86da88 100755 --- a/goldens/size-tracking/aio-payloads.json +++ b/goldens/size-tracking/aio-payloads.json @@ -12,7 +12,7 @@ "aio-local": { "uncompressed": { "runtime": 4325, - "main": 455228, + "main": 456041, "polyfills": 33952, "styles": 73964, "light-theme": 78157, diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index 5d6632ba4f543..1373f178eb366 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -48,7 +48,7 @@ "animations": { "uncompressed": { "runtime": 1070, - "main": 155920, + "main": 156446, "polyfills": 33814 } }, diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/security_sensitive_constant_attributes.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/security_sensitive_constant_attributes.js index bd1068232fead..747c69961a492 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/security_sensitive_constant_attributes.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/security_sensitive_constant_attributes.js @@ -6,7 +6,7 @@ consts: [ ], template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵelement(0, "embed", 0)(1, "iframe", 1)(2, "object", 2)(3, "embed", 0)(4, "img", 3); + $r3$.ɵɵelement(0, "embed", 0)(1, "iframe", 1, null, i0.ɵɵvalidateIframeStaticAttributes)(2, "object", 2)(3, "embed", 0)(4, "img", 3); } … } 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 interpolation)`, + () => { + @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 interpolation)`, + () => { + @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()); + + // 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 not error when a directive sets a security-sensitive host attribute on a non-iframe element', + () => { + @Directive({ + standalone: true, + selector: '[dir]', + host: { + 'src': TEST_IFRAME_URL, + 'sandbox': '', + }, + }) + class Dir { + } + + @Component({ + standalone: true, + imports: [Dir], + selector: 'my-comp', + template: '', + }) + class NonIframeComp { + } + + const fixture = TestBed.createComponent(NonIframeComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.firstChild.src).toEqual(TEST_IFRAME_URL); + }); + + + 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); + }); + }); + }); +});