diff --git a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts index b3cc75f43d84f..6a5cca6664dd8 100644 --- a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts +++ b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Inject, Injectable, Injector, Input, NgModule, NgZone, OnChanges, OnDestroy, OnInit, Renderer2, SimpleChanges, ɵformatRuntimeError as formatRuntimeError, ɵRuntimeError as RuntimeError} from '@angular/core'; +import {Directive, ElementRef, Inject, Injector, Input, NgModule, NgZone, OnChanges, OnDestroy, OnInit, Renderer2, SimpleChanges, ɵ_sanitizeUrl as sanitizeUrl, ɵRuntimeError as RuntimeError} from '@angular/core'; import {RuntimeErrorCode} from '../../errors'; @@ -161,9 +161,15 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy { } this.setHostAttribute('loading', this.getLoadingBehavior()); this.setHostAttribute('fetchpriority', this.getFetchPriority()); + + // Use the `sanitizeUrl` function directly (vs using `DomSanitizer`), + // to make the code more tree-shakable (avoid referencing the entire class). + // The same function is used when regular `src` is used on an `` element. + const src = sanitizeUrl(this.getRewrittenSrc()); + // The `src` and `srcset` attributes should be set last since other attributes // could affect the image's loading behavior. - this.setHostAttribute('src', this.getRewrittenSrc()); + this.setHostAttribute('src', src); if (this.rawSrcset) { this.setHostAttribute('srcset', this.getRewrittenSrcset()); } @@ -204,7 +210,8 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy { const finalSrcs = this.rawSrcset.split(',').filter(src => src !== '').map(srcStr => { srcStr = srcStr.trim(); const width = widthSrcSet ? parseFloat(srcStr) : parseFloat(srcStr) * this.width!; - return `${this.imageLoader({src: this.rawSrc, width})} ${srcStr}`; + const imgSrc = sanitizeUrl(this.imageLoader({src: this.rawSrc, width})); + return `${imgSrc} ${srcStr}`; }); return finalSrcs.join(', '); } diff --git a/packages/common/test/directives/ng_optimized_image_spec.ts b/packages/common/test/directives/ng_optimized_image_spec.ts index dbe1396c4530f..81940d743edda 100644 --- a/packages/common/test/directives/ng_optimized_image_spec.ts +++ b/packages/common/test/directives/ng_optimized_image_spec.ts @@ -472,6 +472,56 @@ describe('Image directive', () => { }); }); + describe('sanitization', () => { + // Loader function that just returns provided `src`. + const noopLoader = (config: ImageLoaderConfig) => config.src; + + it('should apply sanitization to the `rawSrc` with a static value', () => { + setupTestingModule({imageLoader: noopLoader}); + + const template = + ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + const img = fixture.nativeElement.querySelector('img')!; + // The `src` looks like this: 'unsafe:javascript:alert(`Unsafe code execution`)' + expect(img.src.startsWith('unsafe:')).toBe(true); + }); + + it('should apply sanitization to the `rawSrc` when used as a binding', () => { + setupTestingModule({imageLoader: noopLoader}); + + const template = + ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + const img = fixture.nativeElement.querySelector('img')!; + // The `src` looks like this: 'unsafe:javascript:alert(`Unsafe code execution`)' + expect(img.src.startsWith('unsafe:')).toBe(true); + }); + + it('should apply sanitization to the `rawSrcset` value', () => { + setupTestingModule({imageLoader: noopLoader}); + + const template = + ``; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + const nativeElement = fixture.nativeElement as HTMLElement; + const img = nativeElement.querySelector('img')!; + + // The `src` looks like this: 'unsafe:javascript:alert(`Unsafe code execution`)' + expect(img.src.startsWith('unsafe:')).toBe(true); + expect(img.srcset) + .toBe( + 'unsafe:javascript:alert(`Unsafe code execution`) 100w, ' + + 'unsafe:javascript:alert(`Unsafe code execution`) 200w'); + }); + }); + describe('fetch priority', () => { it('should be "high" for priority images', () => { setupTestingModule();