feat(security): allow more HTML5 elements and attributes in sanitizers
Allow more elements and attributes from the HTML5 spec which were stripped by the htmlSanitizer. fixes #9438 feat(security): allow audio data URLs in urlSanitizer test(security) : add test for valid audio data URL feat(security): allow and sanitize srcset attributes test(security): test for srcset sanitization
This commit is contained in:
parent
3644eef860
commit
6605eb30e9
@ -10,7 +10,7 @@ import {isDevMode} from '@angular/core';
|
|||||||
|
|
||||||
import {DomAdapter, getDOM} from '../dom/dom_adapter';
|
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,
|
OPTIONAL_END_TAG_BLOCK_ELEMENTS,
|
||||||
tagSet(
|
tagSet(
|
||||||
'address,article,' +
|
'address,article,' +
|
||||||
'aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' +
|
'aside,blockquote,caption,center,del,details,dialog,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' +
|
||||||
'h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul'));
|
'h6,header,hgroup,hr,ins,main,map,menu,nav,ol,pre,section,summary,table,ul'));
|
||||||
|
|
||||||
// Inline Elements - HTML5
|
// Inline Elements - HTML5
|
||||||
const INLINE_ELEMENTS = merge(
|
const INLINE_ELEMENTS = merge(
|
||||||
OPTIONAL_END_TAG_INLINE_ELEMENTS,
|
OPTIONAL_END_TAG_INLINE_ELEMENTS,
|
||||||
tagSet(
|
tagSet(
|
||||||
'a,abbr,acronym,b,' +
|
'a,abbr,acronym,audio,b,' +
|
||||||
'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,' +
|
'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,span,strike,strong,sub,sup,time,tt,u,var'));
|
'samp,small,source,span,strike,strong,sub,sup,time,track,tt,u,var,video'));
|
||||||
|
|
||||||
const VALID_ELEMENTS =
|
const VALID_ELEMENTS =
|
||||||
merge(VOID_ELEMENTS, BLOCK_ELEMENTS, INLINE_ELEMENTS, OPTIONAL_END_TAG_ELEMENTS);
|
merge(VOID_ELEMENTS, BLOCK_ELEMENTS, INLINE_ELEMENTS, OPTIONAL_END_TAG_ELEMENTS);
|
||||||
|
|
||||||
// Attributes that have href and hence need to be sanitized
|
// 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(
|
const HTML_ATTRS = tagSet(
|
||||||
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
|
'abbr,accesskey,align,alt,autoplay,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,' +
|
||||||
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
|
'compact,controls,coords,datetime,default,dir,download,face,headers,height,hidden,hreflang,hspace,' +
|
||||||
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
|
'ismap,itemscope,itemprop,kind,label,lang,language,loop,media,muted,nohref,nowrap,open,preload,rel,rev,role,rows,rowspan,rules,' +
|
||||||
'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
|
'scope,scrolling,shape,size,sizes,span,srclang,start,summary,tabindex,target,title,translate,type,usemap,' +
|
||||||
'valign,value,vspace,width');
|
'valign,value,vspace,width');
|
||||||
|
|
||||||
// NB: This currently conciously doesn't support SVG. SVG sanitization has had several security
|
// 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
|
// can be sanitized, but they increase security surface area without a legitimate use case, so they
|
||||||
// are left out here.
|
// 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
|
* SanitizingHtmlSerializer serializes a DOM fragment, stripping out any unsafe elements and unsafe
|
||||||
@ -159,6 +162,7 @@ class SanitizingHtmlSerializer {
|
|||||||
if (!VALID_ATTRS.hasOwnProperty(lower)) return;
|
if (!VALID_ATTRS.hasOwnProperty(lower)) return;
|
||||||
// TODO(martinprobst): Special case image URIs for data:image/...
|
// TODO(martinprobst): Special case image URIs for data:image/...
|
||||||
if (URI_ATTRS[lower]) value = sanitizeUrl(value);
|
if (URI_ATTRS[lower]) value = sanitizeUrl(value);
|
||||||
|
if (SRCSET_ATTRS[lower]) value = sanitizeSrcset(value);
|
||||||
this.buf.push(' ');
|
this.buf.push(' ');
|
||||||
this.buf.push(attrName);
|
this.buf.push(attrName);
|
||||||
this.buf.push('="');
|
this.buf.push('="');
|
||||||
|
@ -39,9 +39,12 @@ import {getDOM} from '../dom/dom_adapter';
|
|||||||
*/
|
*/
|
||||||
const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi;
|
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 =
|
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 {
|
export function sanitizeUrl(url: string): string {
|
||||||
url = String(url);
|
url = String(url);
|
||||||
@ -51,3 +54,8 @@ export function sanitizeUrl(url: string): string {
|
|||||||
|
|
||||||
return 'unsafe:' + url;
|
return 'unsafe:' + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizeSrcset(srcset: string): string {
|
||||||
|
srcset = String(srcset);
|
||||||
|
return srcset.split(',').map((srcset) => sanitizeUrl(srcset.trim())).join(', ');
|
||||||
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import * as t from '@angular/core/testing/testing_internal';
|
import * as t from '@angular/core/testing/testing_internal';
|
||||||
|
|
||||||
import {getDOM} from '../../src/dom/dom_adapter';
|
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() {
|
export function main() {
|
||||||
t.describe('URL sanitizer', () => {
|
t.describe('URL sanitizer', () => {
|
||||||
@ -28,7 +28,6 @@ export function main() {
|
|||||||
t.expect(logMsgs.join('\n')).toMatch(/sanitizing unsafe URL value/);
|
t.expect(logMsgs.join('\n')).toMatch(/sanitizing unsafe URL value/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
t.describe('valid URLs', () => {
|
t.describe('valid URLs', () => {
|
||||||
const validUrls = [
|
const validUrls = [
|
||||||
'',
|
'',
|
||||||
@ -47,6 +46,7 @@ export function main() {
|
|||||||
'http://JavaScript/my.js',
|
'http://JavaScript/my.js',
|
||||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', // Truncated.
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', // Truncated.
|
||||||
'data:video/webm;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
|
'data:video/webm;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
|
||||||
|
'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
|
||||||
];
|
];
|
||||||
for (let url of validUrls) {
|
for (let url of validUrls) {
|
||||||
t.it(`valid ${url}`, () => t.expect(sanitizeUrl(url)).toEqual(url));
|
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.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:/));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user