diff --git a/modules/@angular/platform-browser/src/security/html_sanitizer.ts b/modules/@angular/platform-browser/src/security/html_sanitizer.ts index 7b7908c043..3f8cdfcc19 100644 --- a/modules/@angular/platform-browser/src/security/html_sanitizer.ts +++ b/modules/@angular/platform-browser/src/security/html_sanitizer.ts @@ -10,7 +10,7 @@ import {isDevMode} from '@angular/core'; import {DomAdapter, getDOM} from '../dom/dom_adapter'; -import {sanitizeUrl} from './url_sanitizer'; +import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer'; @@ -77,28 +77,31 @@ const BLOCK_ELEMENTS = merge( OPTIONAL_END_TAG_BLOCK_ELEMENTS, tagSet( 'address,article,' + - 'aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' + - 'h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul')); + 'aside,blockquote,caption,center,del,details,dialog,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' + + 'h6,header,hgroup,hr,ins,main,map,menu,nav,ol,pre,section,summary,table,ul')); // Inline Elements - HTML5 const INLINE_ELEMENTS = merge( OPTIONAL_END_TAG_INLINE_ELEMENTS, tagSet( - 'a,abbr,acronym,b,' + - 'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,' + - 'samp,small,span,strike,strong,sub,sup,time,tt,u,var')); + 'a,abbr,acronym,audio,b,' + + 'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,picture,q,ruby,rp,rt,s,' + + 'samp,small,source,span,strike,strong,sub,sup,time,track,tt,u,var,video')); const VALID_ELEMENTS = merge(VOID_ELEMENTS, BLOCK_ELEMENTS, INLINE_ELEMENTS, OPTIONAL_END_TAG_ELEMENTS); // Attributes that have href and hence need to be sanitized -const URI_ATTRS = tagSet('background,cite,href,longdesc,src,xlink:href'); +const URI_ATTRS = tagSet('background,cite,href,itemtype,longdesc,poster,src,xlink:href'); + +// Attributes that have special href set hence need to be sanitized +const SRCSET_ATTRS = tagSet('srcset'); const HTML_ATTRS = tagSet( - 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + - 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + - 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + - 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' + + 'abbr,accesskey,align,alt,autoplay,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,' + + 'compact,controls,coords,datetime,default,dir,download,face,headers,height,hidden,hreflang,hspace,' + + 'ismap,itemscope,itemprop,kind,label,lang,language,loop,media,muted,nohref,nowrap,open,preload,rel,rev,role,rows,rowspan,rules,' + + 'scope,scrolling,shape,size,sizes,span,srclang,start,summary,tabindex,target,title,translate,type,usemap,' + 'valign,value,vspace,width'); // NB: This currently conciously doesn't support SVG. SVG sanitization has had several security @@ -109,7 +112,7 @@ const HTML_ATTRS = tagSet( // can be sanitized, but they increase security surface area without a legitimate use case, so they // are left out here. -const VALID_ATTRS = merge(URI_ATTRS, HTML_ATTRS); +const VALID_ATTRS = merge(URI_ATTRS, SRCSET_ATTRS, HTML_ATTRS); /** * SanitizingHtmlSerializer serializes a DOM fragment, stripping out any unsafe elements and unsafe @@ -159,6 +162,7 @@ class SanitizingHtmlSerializer { if (!VALID_ATTRS.hasOwnProperty(lower)) return; // TODO(martinprobst): Special case image URIs for data:image/... if (URI_ATTRS[lower]) value = sanitizeUrl(value); + if (SRCSET_ATTRS[lower]) value = sanitizeSrcset(value); this.buf.push(' '); this.buf.push(attrName); this.buf.push('="'); diff --git a/modules/@angular/platform-browser/src/security/url_sanitizer.ts b/modules/@angular/platform-browser/src/security/url_sanitizer.ts index 3d1dcf9fe3..dd5a9f9d74 100644 --- a/modules/@angular/platform-browser/src/security/url_sanitizer.ts +++ b/modules/@angular/platform-browser/src/security/url_sanitizer.ts @@ -39,9 +39,12 @@ import {getDOM} from '../dom/dom_adapter'; */ const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi; -/** A pattern that matches safe data URLs. Only matches image and video types. */ +/* A pattern that matches safe srcset values */ +const SAFE_SRCSET_PATTERN = /^(?:(?:https?|file):|[^&:/?#]*(?:[/?#]|$))/gi; + +/** A pattern that matches safe data URLs. Only matches image, video and audio types. */ const DATA_URL_PATTERN = - /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm));base64,[a-z0-9+\/]+=*$/i; + /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+\/]+=*$/i; export function sanitizeUrl(url: string): string { url = String(url); @@ -51,3 +54,8 @@ export function sanitizeUrl(url: string): string { return 'unsafe:' + url; } + +export function sanitizeSrcset(srcset: string): string { + srcset = String(srcset); + return srcset.split(',').map((srcset) => sanitizeUrl(srcset.trim())).join(', '); +} diff --git a/modules/@angular/platform-browser/test/security/url_sanitizer_spec.ts b/modules/@angular/platform-browser/test/security/url_sanitizer_spec.ts index 35af11e405..aaf414e1d3 100644 --- a/modules/@angular/platform-browser/test/security/url_sanitizer_spec.ts +++ b/modules/@angular/platform-browser/test/security/url_sanitizer_spec.ts @@ -9,7 +9,7 @@ import * as t from '@angular/core/testing/testing_internal'; import {getDOM} from '../../src/dom/dom_adapter'; -import {sanitizeUrl} from '../../src/security/url_sanitizer'; +import {sanitizeSrcset, sanitizeUrl} from '../../src/security/url_sanitizer'; export function main() { t.describe('URL sanitizer', () => { @@ -28,7 +28,6 @@ export function main() { t.expect(logMsgs.join('\n')).toMatch(/sanitizing unsafe URL value/); }); - t.describe('valid URLs', () => { const validUrls = [ '', @@ -47,6 +46,7 @@ export function main() { 'http://JavaScript/my.js', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', // Truncated. 'data:video/webm;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', ]; for (let url of validUrls) { t.it(`valid ${url}`, () => t.expect(sanitizeUrl(url)).toEqual(url)); @@ -76,5 +76,43 @@ export function main() { t.it(`valid ${url}`, () => t.expect(sanitizeUrl(url)).toMatch(/^unsafe:/)); } }); + + t.describe('valid srcsets', () => { + const validSrcsets = [ + '', + 'http://angular.io/images/test.png', + 'http://angular.io/images/test.png, http://angular.io/images/test.png', + 'http://angular.io/images/test.png, http://angular.io/images/test.png, http://angular.io/images/test.png', + 'http://angular.io/images/test.png 2x', + 'http://angular.io/images/test.png 2x, http://angular.io/images/test.png 3x', + 'http://angular.io/images/test.png 1.5x', + 'http://angular.io/images/test.png 1.25x', + 'http://angular.io/images/test.png 200w, http://angular.io/images/test.png 300w', + 'https://angular.io/images/test.png, http://angular.io/images/test.png', + 'http://angular.io:80/images/test.png, http://angular.io:8080/images/test.png', + 'http://www.angular.io:80/images/test.png, http://www.angular.io:8080/images/test.png', + 'https://angular.io/images/test.png, https://angular.io/images/test.png', + '//angular.io/images/test.png, //angular.io/images/test.png', + '/images/test.png, /images/test.png', + 'images/test.png, images/test.png', + 'http://angular.io/images/test.png?12345, http://angular.io/images/test.png?12345', + 'http://angular.io/images/test.png?maxage, http://angular.io/images/test.png?maxage', + 'http://angular.io/images/test.png?maxage=234, http://angular.io/images/test.png?maxage=234', + ]; + for (let srcset of validSrcsets) { + t.it(`valid ${srcset}`, () => t.expect(sanitizeSrcset(srcset)).toEqual(srcset)); + } + }); + + t.describe('invalid srcsets', () => { + const invalidSrcsets = [ + 'ht:tp://angular.io/images/test.png', + 'http://angular.io/images/test.png, ht:tp://angular.io/images/test.png', + ]; + for (let srcset of invalidSrcsets) { + t.it(`valid ${srcset}`, () => t.expect(sanitizeSrcset(srcset)).toMatch(/unsafe:/)); + } + }); + }); }