Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
018750154d | |||
b19216d58b | |||
84fc1a3663 | |||
1c40be26c6 | |||
2c5cf19c6d | |||
0dacf6d5f1 | |||
e9f1d44015 | |||
1d9024ee9a | |||
6a6164ab4f |
@ -1,12 +1,10 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
# force trusty as Google Chrome addon is not supported on Precise
|
||||
dist: trusty
|
||||
node_js:
|
||||
- '6.9.5'
|
||||
|
||||
addons:
|
||||
chrome: stable
|
||||
# firefox: "38.0"
|
||||
apt:
|
||||
sources:
|
||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,3 +1,13 @@
|
||||
<a name="4.4.7"></a>
|
||||
## [4.4.7](https://github.com/angular/angular/compare/4.4.6...4.4.7) (2018-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** use appropriate inert document strategy for Firefox & Safari ([#22077](https://github.com/angular/angular/issues/22077)) ([2c5cf19](https://github.com/angular/angular/commit/2c5cf19))
|
||||
|
||||
|
||||
|
||||
<a name="4.4.6"></a>
|
||||
## [4.4.6](https://github.com/angular/angular/compare/4.4.5...4.4.6) (2017-10-18)
|
||||
|
||||
|
@ -30,8 +30,14 @@ module.exports = function (config) {
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
browsers: ['CustomChrome'],
|
||||
browserNoActivityTimeout: 60000,
|
||||
singleRun: false
|
||||
singleRun: false,
|
||||
customLaunchers: {
|
||||
CustomChrome: {
|
||||
base: 'Chrome',
|
||||
flags: process.env.TRAVIS && ['--no-sandbox']
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -57,7 +57,7 @@
|
||||
"~~check-env": "node scripts/check-environment",
|
||||
"~~build": "ng build --target=production --environment=stable -sm --build-optimizer",
|
||||
"post~~build": "yarn sw-manifest && yarn sw-copy",
|
||||
"~~update-webdriver": "webdriver-manager update --standalone false --gecko false"
|
||||
"~~update-webdriver": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.5 <7.0.0",
|
||||
|
@ -12,7 +12,8 @@ exports.config = {
|
||||
browserName: 'chrome',
|
||||
// For Travis
|
||||
chromeOptions: {
|
||||
binary: process.env.CHROME_BIN
|
||||
binary: process.env.CHROME_BIN,
|
||||
args: ['--no-sandbox']
|
||||
}
|
||||
},
|
||||
directConnect: true,
|
||||
|
@ -67,7 +67,7 @@ case $deployEnv in
|
||||
readonly firebaseToken=$FIREBASE_TOKEN
|
||||
;;
|
||||
archive)
|
||||
readonly projectId=angular-io-${majorVersion}
|
||||
readonly projectId=v${majorVersion}-angular-io
|
||||
readonly deployedUrl=https://v${majorVersion}.angular.io/
|
||||
readonly firebaseToken=$FIREBASE_TOKEN
|
||||
;;
|
||||
|
@ -94,7 +94,7 @@ Deployment URL : https://angular.io/"
|
||||
)
|
||||
expected="Git branch : 2.4.x
|
||||
Build/deploy mode : archive
|
||||
Firebase project : angular-io-2
|
||||
Firebase project : v2-angular-io
|
||||
Deployment URL : https://v2.angular.io/"
|
||||
check "$actual" "$expected"
|
||||
)
|
||||
|
@ -17,8 +17,16 @@ const printer = require('lighthouse/lighthouse-cli/printer');
|
||||
const config = require('lighthouse/lighthouse-core/config/default.js');
|
||||
|
||||
// Constants
|
||||
const CHROME_LAUNCH_OPTS = {};
|
||||
const VIEWER_URL = 'https://googlechrome.github.io/lighthouse/viewer/';
|
||||
|
||||
|
||||
// Specify the path and flags for Chrome on Travis
|
||||
if (process.env.TRAVIS) {
|
||||
process.env.LIGHTHOUSE_CHROMIUM_PATH = process.env.CHROME_BIN;
|
||||
CHROME_LAUNCH_OPTS.chromeFlags = ['--no-sandbox'];
|
||||
}
|
||||
|
||||
// Run
|
||||
_main(process.argv.slice(2));
|
||||
|
||||
@ -66,7 +74,7 @@ function ignoreHttpsAudits(config) {
|
||||
}
|
||||
|
||||
function launchChromeAndRunLighthouse(url, flags, config) {
|
||||
return chromeLauncher.launch().then(chrome => {
|
||||
return chromeLauncher.launch(CHROME_LAUNCH_OPTS).then(chrome => {
|
||||
flags.port = chrome.port;
|
||||
return lighthouse(url, flags, config).
|
||||
then(results => chrome.kill().then(() => results)).
|
||||
|
@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"http-server": "http-server",
|
||||
"protractor": "protractor",
|
||||
"webdriver:update": "webdriver-manager update --standalone false --gecko false",
|
||||
"webdriver:update": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG",
|
||||
"postinstall": "yarn webdriver:update"
|
||||
},
|
||||
"keywords": [],
|
||||
|
@ -20,7 +20,12 @@ exports.config = {
|
||||
|
||||
// Capabilities to be passed to the webdriver instance.
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
'browserName': 'chrome',
|
||||
// For Travis
|
||||
chromeOptions: {
|
||||
binary: process.env.CHROME_BIN,
|
||||
args: ['--no-sandbox']
|
||||
}
|
||||
},
|
||||
|
||||
// Framework to use. Jasmine is recommended.
|
||||
|
@ -23,7 +23,7 @@
|
||||
"protractor": "file:../../node_modules/protractor"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
|
||||
"closure": "java -jar node_modules/google-closure-compiler/compiler.jar --flagfile closure.conf",
|
||||
"test": "ngc && yarn run closure && concurrently \"yarn run serve\" \"yarn run protractor\" --kill-others --success first",
|
||||
"serve": "lite-server -c e2e/browser.config.json",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
|
||||
"test": "concurrently \"npm run serve\" \"npm run protractor\" --kill-others --success first",
|
||||
"serve": "lite-server -c bs-config.e2e.json",
|
||||
"preprotractor": "tsc -p e2e",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "4.4.6",
|
||||
"version": "4.4.7",
|
||||
"private": true,
|
||||
"branchPattern": "2.0.*",
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
@ -18,7 +18,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('Please use Yarn instead of NPM to install dependencies. See: https://yarnpkg.com/lang/en/docs/install/')\"",
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "yarn update-webdriver",
|
||||
"update-webdriver": "webdriver-manager update --gecko false $CHROMEDRIVER_VERSION_ARG",
|
||||
"check-env": "gulp check-env"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -10,35 +10,9 @@ import {isDevMode} from '@angular/core';
|
||||
|
||||
import {DomAdapter, getDOM} from '../dom/dom_adapter';
|
||||
|
||||
import {InertBodyHelper} from './inert_body';
|
||||
import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer';
|
||||
|
||||
/** A <body> element that can be safely used to parse untrusted HTML. Lazily initialized below. */
|
||||
let inertElement: HTMLElement|null = null;
|
||||
/** Lazily initialized to make sure the DOM adapter gets set before use. */
|
||||
let DOM: DomAdapter = null !;
|
||||
|
||||
/** Returns an HTML element that is guaranteed to not execute code when creating elements in it. */
|
||||
function getInertElement() {
|
||||
if (inertElement) return inertElement;
|
||||
DOM = getDOM();
|
||||
|
||||
// Prefer using <template> element if supported.
|
||||
const templateEl = DOM.createElement('template');
|
||||
if ('content' in templateEl) return templateEl;
|
||||
|
||||
const doc = DOM.createHtmlDocument();
|
||||
inertElement = DOM.querySelector(doc, 'body');
|
||||
if (inertElement == null) {
|
||||
// usually there should be only one body element in the document, but IE doesn't have any, so we
|
||||
// need to create one.
|
||||
const html = DOM.createElement('html', doc);
|
||||
inertElement = DOM.createElement('body', doc);
|
||||
DOM.appendChild(html, inertElement);
|
||||
DOM.appendChild(doc, html);
|
||||
}
|
||||
return inertElement;
|
||||
}
|
||||
|
||||
function tagSet(tags: string): {[k: string]: boolean} {
|
||||
const res: {[k: string]: boolean} = {};
|
||||
for (const t of tags.split(',')) res[t] = true;
|
||||
@ -121,53 +95,54 @@ class SanitizingHtmlSerializer {
|
||||
// because characters were re-encoded.
|
||||
public sanitizedSomething = false;
|
||||
private buf: string[] = [];
|
||||
private DOM = getDOM();
|
||||
|
||||
sanitizeChildren(el: Element): string {
|
||||
// This cannot use a TreeWalker, as it has to run on Angular's various DOM adapters.
|
||||
// However this code never accesses properties off of `document` before deleting its contents
|
||||
// again, so it shouldn't be vulnerable to DOM clobbering.
|
||||
let current: Node = el.firstChild !;
|
||||
let current: Node = this.DOM.firstChild(el) !;
|
||||
while (current) {
|
||||
if (DOM.isElementNode(current)) {
|
||||
if (this.DOM.isElementNode(current)) {
|
||||
this.startElement(current as Element);
|
||||
} else if (DOM.isTextNode(current)) {
|
||||
this.chars(DOM.nodeValue(current) !);
|
||||
} else if (this.DOM.isTextNode(current)) {
|
||||
this.chars(this.DOM.nodeValue(current) !);
|
||||
} else {
|
||||
// Strip non-element, non-text nodes.
|
||||
this.sanitizedSomething = true;
|
||||
}
|
||||
if (DOM.firstChild(current)) {
|
||||
current = DOM.firstChild(current) !;
|
||||
if (this.DOM.firstChild(current)) {
|
||||
current = this.DOM.firstChild(current) !;
|
||||
continue;
|
||||
}
|
||||
while (current) {
|
||||
// Leaving the element. Walk up and to the right, closing tags as we go.
|
||||
if (DOM.isElementNode(current)) {
|
||||
if (this.DOM.isElementNode(current)) {
|
||||
this.endElement(current as Element);
|
||||
}
|
||||
|
||||
let next = checkClobberedElement(current, DOM.nextSibling(current) !);
|
||||
let next = this.checkClobberedElement(current, this.DOM.nextSibling(current) !);
|
||||
|
||||
if (next) {
|
||||
current = next;
|
||||
break;
|
||||
}
|
||||
|
||||
current = checkClobberedElement(current, DOM.parentElement(current) !);
|
||||
current = this.checkClobberedElement(current, this.DOM.parentElement(current) !);
|
||||
}
|
||||
}
|
||||
return this.buf.join('');
|
||||
}
|
||||
|
||||
private startElement(element: Element) {
|
||||
const tagName = DOM.nodeName(element).toLowerCase();
|
||||
const tagName = this.DOM.nodeName(element).toLowerCase();
|
||||
if (!VALID_ELEMENTS.hasOwnProperty(tagName)) {
|
||||
this.sanitizedSomething = true;
|
||||
return;
|
||||
}
|
||||
this.buf.push('<');
|
||||
this.buf.push(tagName);
|
||||
DOM.attributeMap(element).forEach((value: string, attrName: string) => {
|
||||
this.DOM.attributeMap(element).forEach((value: string, attrName: string) => {
|
||||
const lower = attrName.toLowerCase();
|
||||
if (!VALID_ATTRS.hasOwnProperty(lower)) {
|
||||
this.sanitizedSomething = true;
|
||||
@ -186,7 +161,7 @@ class SanitizingHtmlSerializer {
|
||||
}
|
||||
|
||||
private endElement(current: Element) {
|
||||
const tagName = DOM.nodeName(current).toLowerCase();
|
||||
const tagName = this.DOM.nodeName(current).toLowerCase();
|
||||
if (VALID_ELEMENTS.hasOwnProperty(tagName) && !VOID_ELEMENTS.hasOwnProperty(tagName)) {
|
||||
this.buf.push('</');
|
||||
this.buf.push(tagName);
|
||||
@ -195,14 +170,14 @@ class SanitizingHtmlSerializer {
|
||||
}
|
||||
|
||||
private chars(chars: string) { this.buf.push(encodeEntities(chars)); }
|
||||
}
|
||||
|
||||
function checkClobberedElement(node: Node, nextNode: Node): Node {
|
||||
if (nextNode && DOM.contains(node, nextNode)) {
|
||||
throw new Error(
|
||||
`Failed to sanitize html because the element is clobbered: ${DOM.getOuterHTML(node)}`);
|
||||
checkClobberedElement(node: Node, nextNode: Node): Node {
|
||||
if (nextNode && this.DOM.contains(node, nextNode)) {
|
||||
throw new Error(
|
||||
`Failed to sanitize html because the element is clobbered: ${this.DOM.getOuterHTML(node)}`);
|
||||
}
|
||||
return nextNode;
|
||||
}
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
// Regular Expressions for parsing tags and attributes
|
||||
@ -233,33 +208,20 @@ function encodeEntities(value: string) {
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1'
|
||||
* attribute to declare ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo').
|
||||
*
|
||||
* This is undesirable since we don't want to allow any of these custom attributes. This method
|
||||
* strips them all.
|
||||
*/
|
||||
function stripCustomNsAttrs(el: Element) {
|
||||
DOM.attributeMap(el).forEach((_, attrName) => {
|
||||
if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
|
||||
DOM.removeAttribute(el, attrName);
|
||||
}
|
||||
});
|
||||
for (const n of DOM.childNodesAsList(el)) {
|
||||
if (DOM.isElementNode(n)) stripCustomNsAttrs(n as Element);
|
||||
}
|
||||
}
|
||||
let inertBodyHelper: InertBodyHelper;
|
||||
|
||||
/**
|
||||
* Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to
|
||||
* the DOM in a browser environment.
|
||||
*/
|
||||
export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||
const DOM = getDOM();
|
||||
let inertBodyElement: HTMLElement|null = null;
|
||||
try {
|
||||
const containerEl = getInertElement();
|
||||
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc, DOM);
|
||||
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
|
||||
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
|
||||
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||
|
||||
// mXSS protection. Repeatedly parse the document to make sure it stabilizes, so that a browser
|
||||
// trying to auto-correct incorrect HTML cannot cause formerly inert HTML to become dangerous.
|
||||
@ -273,31 +235,25 @@ export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||
mXSSAttempts--;
|
||||
|
||||
unsafeHtml = parsedHtml;
|
||||
DOM.setInnerHTML(containerEl, unsafeHtml);
|
||||
if (defaultDoc.documentMode) {
|
||||
// strip custom-namespaced attributes on IE<=11
|
||||
stripCustomNsAttrs(containerEl);
|
||||
}
|
||||
parsedHtml = DOM.getInnerHTML(containerEl);
|
||||
parsedHtml = DOM.getInnerHTML(inertBodyElement);
|
||||
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||
} while (unsafeHtml !== parsedHtml);
|
||||
|
||||
const sanitizer = new SanitizingHtmlSerializer();
|
||||
const safeHtml = sanitizer.sanitizeChildren(DOM.getTemplateContent(containerEl) || containerEl);
|
||||
|
||||
// Clear out the body element.
|
||||
const parent = DOM.getTemplateContent(containerEl) || containerEl;
|
||||
for (const child of DOM.childNodesAsList(parent)) {
|
||||
DOM.removeChild(parent, child);
|
||||
}
|
||||
|
||||
const safeHtml =
|
||||
sanitizer.sanitizeChildren(DOM.getTemplateContent(inertBodyElement) || inertBodyElement);
|
||||
if (isDevMode() && sanitizer.sanitizedSomething) {
|
||||
DOM.log('WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss).');
|
||||
}
|
||||
|
||||
return safeHtml;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
// In case anything goes wrong, clear out inertElement to reset the entire DOM structure.
|
||||
inertElement = null;
|
||||
throw e;
|
||||
if (inertBodyElement) {
|
||||
const parent = DOM.getTemplateContent(inertBodyElement) || inertBodyElement;
|
||||
for (const child of DOM.childNodesAsList(parent)) {
|
||||
DOM.removeChild(parent, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
171
packages/platform-browser/src/security/inert_body.ts
Normal file
171
packages/platform-browser/src/security/inert_body.ts
Normal file
@ -0,0 +1,171 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DomAdapter, getDOM} from '../dom/dom_adapter';
|
||||
|
||||
/**
|
||||
* This helper class is used to get hold of an inert tree of DOM elements containing dirty HTML
|
||||
* that needs sanitizing.
|
||||
* Depending upon browser support we must use one of three strategies for doing this.
|
||||
* Support: Safari 10.x -> XHR strategy
|
||||
* Support: Firefox -> DomParser strategy
|
||||
* Default: InertDocument strategy
|
||||
*/
|
||||
export class InertBodyHelper {
|
||||
private inertBodyElement: HTMLElement;
|
||||
|
||||
constructor(private defaultDoc: any, private DOM: DomAdapter) {
|
||||
const inertDocument = this.DOM.createHtmlDocument();
|
||||
this.inertBodyElement = inertDocument.body;
|
||||
|
||||
if (this.inertBodyElement == null) {
|
||||
// usually there should be only one body element in the document, but IE doesn't have any, so
|
||||
// we need to create one.
|
||||
const inertHtml = this.DOM.createElement('html', inertDocument);
|
||||
this.inertBodyElement = this.DOM.createElement('body', inertDocument);
|
||||
this.DOM.appendChild(inertHtml, this.inertBodyElement);
|
||||
this.DOM.appendChild(inertDocument, inertHtml);
|
||||
}
|
||||
|
||||
this.DOM.setInnerHTML(
|
||||
this.inertBodyElement, '<svg><g onload="this.parentNode.remove()"></g></svg>');
|
||||
if (this.inertBodyElement.querySelector && !this.inertBodyElement.querySelector('svg')) {
|
||||
// We just hit the Safari 10.1 bug - which allows JS to run inside the SVG G element
|
||||
// so use the XHR strategy.
|
||||
this.getInertBodyElement = this.getInertBodyElement_XHR;
|
||||
return;
|
||||
}
|
||||
|
||||
this.DOM.setInnerHTML(
|
||||
this.inertBodyElement, '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">');
|
||||
if (this.inertBodyElement.querySelector && this.inertBodyElement.querySelector('svg img')) {
|
||||
// We just hit the Firefox bug - which prevents the inner img JS from being sanitized
|
||||
// so use the DOMParser strategy, if it is available.
|
||||
// If the DOMParser is not available then we are not in Firefox (Server/WebWorker?) so we
|
||||
// fall through to the default strategy below.
|
||||
if (isDOMParserAvailable()) {
|
||||
this.getInertBodyElement = this.getInertBodyElement_DOMParser;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// None of the bugs were hit so it is safe for us to use the default InertDocument strategy
|
||||
this.getInertBodyElement = this.getInertBodyElement_InertDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an inert DOM element containing DOM created from the dirty HTML string provided.
|
||||
* The implementation of this is determined in the constructor, when the class is instantiated.
|
||||
*/
|
||||
getInertBodyElement: (html: string) => HTMLElement | null;
|
||||
|
||||
/**
|
||||
* Use XHR to create and fill an inert body element (on Safari 10.1)
|
||||
* See
|
||||
* https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
|
||||
*/
|
||||
private getInertBodyElement_XHR(html: string) {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
html = '<body><remove></remove>' + html + '</body>';
|
||||
try {
|
||||
html = encodeURI(html);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'document';
|
||||
xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
|
||||
xhr.send(null);
|
||||
const body: HTMLBodyElement = xhr.response.body;
|
||||
body.removeChild(body.firstChild !);
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use DOMParser to create and fill an inert body element (on Firefox)
|
||||
* See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
|
||||
*
|
||||
*/
|
||||
private getInertBodyElement_DOMParser(html: string) {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
html = '<body><remove></remove>' + html + '</body>';
|
||||
try {
|
||||
const body = new (window as any)
|
||||
.DOMParser()
|
||||
.parseFromString(html, 'text/html')
|
||||
.body as HTMLBodyElement;
|
||||
body.removeChild(body.firstChild !);
|
||||
return body;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
||||
* `createHtmlDocument` to create and fill an inert DOM element.
|
||||
* This is the default sane strategy to use if the browser does not require one of the specialised
|
||||
* strategies above.
|
||||
*/
|
||||
private getInertBodyElement_InertDocument(html: string) {
|
||||
// Prefer using <template> element if supported.
|
||||
const templateEl = this.DOM.createElement('template');
|
||||
if ('content' in templateEl) {
|
||||
this.DOM.setInnerHTML(templateEl, html);
|
||||
return templateEl;
|
||||
}
|
||||
|
||||
this.DOM.setInnerHTML(this.inertBodyElement, html);
|
||||
|
||||
// Support: IE 9-11 only
|
||||
// strip custom-namespaced attributes on IE<=11
|
||||
if (this.defaultDoc.documentMode) {
|
||||
this.stripCustomNsAttrs(this.inertBodyElement);
|
||||
}
|
||||
|
||||
return this.inertBodyElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1'
|
||||
* attribute to declare ns1 namespace and prefixes the attribute with 'ns1' (e.g.
|
||||
* 'ns1:xlink:foo').
|
||||
*
|
||||
* This is undesirable since we don't want to allow any of these custom attributes. This method
|
||||
* strips them all.
|
||||
*/
|
||||
private stripCustomNsAttrs(el: Element) {
|
||||
this.DOM.attributeMap(el).forEach((_, attrName) => {
|
||||
if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
|
||||
this.DOM.removeAttribute(el, attrName);
|
||||
}
|
||||
});
|
||||
for (const n of this.DOM.childNodesAsList(el)) {
|
||||
if (this.DOM.isElementNode(n)) this.stripCustomNsAttrs(n as Element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to determine whether the DOMParser exists in the global context.
|
||||
* The try-catch is because, on some browsers, trying to access this property
|
||||
* on window can actually throw an error.
|
||||
*
|
||||
* @suppress {uselessCode}
|
||||
*/
|
||||
function isDOMParserAvailable() {
|
||||
try {
|
||||
return !!(window as any).DOMParser;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -134,6 +134,32 @@ export function main() {
|
||||
}
|
||||
});
|
||||
|
||||
// See
|
||||
// https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
|
||||
it('should not allow JavaScript execution when creating inert document', () => {
|
||||
const output = sanitizeHtml(defaultDoc, '<svg><g onload="window.xxx = 100"></g></svg>');
|
||||
const window = defaultDoc.defaultView;
|
||||
if (window) {
|
||||
expect(window.xxx).toBe(undefined);
|
||||
window.xxx = undefined;
|
||||
}
|
||||
expect(output).toEqual('');
|
||||
});
|
||||
|
||||
// See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
|
||||
it('should not allow JavaScript hidden in badly formed HTML to get through sanitization (Firefox bug)',
|
||||
() => {
|
||||
debugger;
|
||||
expect(sanitizeHtml(
|
||||
defaultDoc, '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">'))
|
||||
.toEqual(
|
||||
isDOMParserAvailable() ?
|
||||
// PlatformBrowser output
|
||||
'<p><img src="<img src="x"></p>' :
|
||||
// PlatformServer output
|
||||
'<p><img src="</style><img src=x onerror=alert(1)//"></p>');
|
||||
});
|
||||
|
||||
if (browserDetection.isWebkit) {
|
||||
it('should prevent mXSS attacks', function() {
|
||||
expect(sanitizeHtml(defaultDoc, '<a href=" javascript:alert(1)">CLICKME</a>'))
|
||||
@ -142,3 +168,18 @@ export function main() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to determine whether the DOMParser exists in the global context.
|
||||
* The try-catch is because, on some browsers, trying to access this property
|
||||
* on window can actually throw an error.
|
||||
*
|
||||
* @suppress {uselessCode}
|
||||
*/
|
||||
function isDOMParserAvailable() {
|
||||
try {
|
||||
return !!(window as any).DOMParser;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -34,10 +34,11 @@
|
||||
"webpack": "^2.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
|
||||
"build": "./build.sh",
|
||||
"test": "npm run build && concurrently \"npm run serve\" \"npm run protractor\" --kill-others --success first",
|
||||
"serve": "node built/server-bundle.js",
|
||||
"preprotractor": "webdriver-manager update --gecko false && tsc -p e2e",
|
||||
"preprotractor": "tsc -p e2e",
|
||||
"protractor": "protractor e2e/protractor.config.js"
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ function setEnvVar() {
|
||||
if [[ ${print} == "print" ]]; then
|
||||
echo ${name}=${value}
|
||||
fi
|
||||
export ${name}=${value}
|
||||
export ${name}="${value}"
|
||||
}
|
||||
|
||||
# use BASH_SOURCE so that we get the right path when this script is called AND source-d
|
||||
@ -36,6 +36,10 @@ fi
|
||||
|
||||
setEnvVar NODE_VERSION 6.9.5
|
||||
setEnvVar YARN_VERSION 1.0.2
|
||||
# Pin to a Chromium version that does not cause the aio e2e tests to flake. (See https://github.com/angular/angular/pull/20403.)
|
||||
# Revision 494239 (which was part of Chrome 62.0.3186.0) is the last version that does not cause flakes. (Latest revision checked: 508578)
|
||||
setEnvVar CHROMIUM_VERSION 494239 # Chrome 62 linux stable, see https://www.chromium.org/developers/calendar
|
||||
setEnvVar CHROMEDRIVER_VERSION_ARG "--versions.chrome 2.33"
|
||||
setEnvVar SAUCE_CONNECT_VERSION 4.4.9
|
||||
setEnvVar PROJECT_ROOT $(cd ${thisDir}/../..; pwd)
|
||||
|
||||
@ -101,6 +105,7 @@ if [[ ${TRAVIS:-} ]]; then
|
||||
setEnvVar BROWSER_STACK_USERNAME angularteam1
|
||||
# not using use setEnvVar so that we don't print the key
|
||||
export BROWSER_STACK_ACCESS_KEY=BWCd4SynLzdDcv8xtzsB
|
||||
setEnvVar CHROME_BIN ${HOME}/.chrome/chromium/chrome-linux/chrome
|
||||
setEnvVar BROWSER_PROVIDER_READY_FILE /tmp/angular-build/browser-provider-tunnel-init.lock
|
||||
fi
|
||||
|
||||
|
84
scripts/ci/install-chromium.sh
Executable file
84
scripts/ci/install-chromium.sh
Executable file
@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -u -e -o pipefail
|
||||
|
||||
# Setup environment
|
||||
readonly thisDir=$(cd $(dirname $0); pwd)
|
||||
source ${thisDir}/_travis-fold.sh
|
||||
|
||||
|
||||
# This script basically follows the instructions to download an old version of Chromium: https://www.chromium.org/getting-involved/download-chromium
|
||||
# 1) It retrieves the current stable version number from https://www.chromium.org/developers/calendar (via the https://omahaproxy.appspot.com/all file), e.g. 359700 for Chromium 48.
|
||||
# 2) It checks the Travis cache for this specific version
|
||||
# 3) If not available, it downloads and caches it, using the "decrement commit number" trick.
|
||||
|
||||
#Build version read from the OmahaProxy CSV Viewer at https://www.chromium.org/developers/calendar
|
||||
#Let's use the following version of Chromium, and inform about availability of newer build from https://omahaproxy.appspot.com/all
|
||||
#
|
||||
# CHROMIUM_VERSION <<< this variable is now set via env.sh
|
||||
|
||||
PLATFORM="$(uname -s)"
|
||||
case "$PLATFORM" in
|
||||
(Darwin)
|
||||
ARCHITECTURE=Mac
|
||||
DIST_FILE=chrome-mac.zip
|
||||
;;
|
||||
(Linux)
|
||||
ARCHITECTURE=Linux_x64
|
||||
DIST_FILE=chrome-linux.zip
|
||||
;;
|
||||
(*)
|
||||
echo Unsupported platform $PLATFORM. Exiting ... >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
|
||||
TMP=$(curl -s "https://omahaproxy.appspot.com/all") || true
|
||||
oldIFS="$IFS"
|
||||
IFS='
|
||||
'
|
||||
IFS=${IFS:0:1}
|
||||
lines=( $TMP )
|
||||
IFS=','
|
||||
for line in "${lines[@]}"
|
||||
do
|
||||
lineArray=($line);
|
||||
if [ "${lineArray[0]}" = "linux" ] && [ "${lineArray[1]}" = "stable" ] ; then
|
||||
LATEST_CHROMIUM_VERSION="${lineArray[7]}"
|
||||
fi
|
||||
done
|
||||
IFS="$oldIFS"
|
||||
|
||||
CHROMIUM_DIR=$HOME/.chrome/chromium
|
||||
CHROMIUM_BIN=$CHROMIUM_DIR/chrome-linux/chrome
|
||||
CHROMIUM_VERSION_FILE=$CHROMIUM_DIR/VERSION
|
||||
|
||||
EXISTING_VERSION=""
|
||||
if [[ -f $CHROMIUM_VERSION_FILE && -x $CHROMIUM_BIN ]]; then
|
||||
EXISTING_VERSION=`cat $CHROMIUM_VERSION_FILE`
|
||||
echo Found cached Chromium version: ${EXISTING_VERSION}
|
||||
fi
|
||||
|
||||
if [[ "$EXISTING_VERSION" != "$CHROMIUM_VERSION" ]]; then
|
||||
echo Downloading Chromium version: ${CHROMIUM_VERSION}
|
||||
rm -fR $CHROMIUM_DIR
|
||||
mkdir -p $CHROMIUM_DIR
|
||||
|
||||
NEXT=$CHROMIUM_VERSION
|
||||
FILE="chrome-linux.zip"
|
||||
STATUS=404
|
||||
while [[ $STATUS == 404 && $NEXT -ge 0 ]]
|
||||
do
|
||||
echo Fetch Chromium version: ${NEXT}
|
||||
STATUS=$(curl "https://storage.googleapis.com/chromium-browser-snapshots/${ARCHITECTURE}/${NEXT}/${DIST_FILE}" -s -w %{http_code} --create-dirs -o $FILE) || true
|
||||
NEXT=$[$NEXT-1]
|
||||
done
|
||||
|
||||
unzip $FILE -d $CHROMIUM_DIR
|
||||
rm $FILE
|
||||
echo $CHROMIUM_VERSION > $CHROMIUM_VERSION_FILE
|
||||
fi
|
||||
|
||||
if [[ "$CHROMIUM_VERSION" != "$LATEST_CHROMIUM_VERSION" ]]; then
|
||||
echo "New version of Chromium available. Update install-chromium.sh with build number: ${LATEST_CHROMIUM_VERSION}"
|
||||
fi
|
@ -35,7 +35,7 @@ travisFoldEnd "install-yarn"
|
||||
|
||||
# Install all npm dependencies according to yarn.lock
|
||||
travisFoldStart "yarn-install"
|
||||
node tools/npm/check-node-modules --purge || yarn install --freeze-lockfile --non-interactive
|
||||
(node tools/npm/check-node-modules --purge && yarn update-webdriver) || yarn install --frozen-lockfile --non-interactive
|
||||
travisFoldEnd "yarn-install"
|
||||
|
||||
|
||||
@ -64,11 +64,21 @@ if [[ ${TRAVIS} && ${CI_MODE} == "bazel" ]]; then
|
||||
travisFoldEnd "bazel-install"
|
||||
fi
|
||||
|
||||
# Start xvfb for local Chrome testing
|
||||
if [[ ${TRAVIS} && (${CI_MODE} == "js" || ${CI_MODE} == "e2e" || ${CI_MODE} == "e2e_2" || ${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e") ]]; then
|
||||
travisFoldStart "xvfb-start"
|
||||
sh -e /etc/init.d/xvfb start
|
||||
travisFoldEnd "xvfb-start"
|
||||
|
||||
# Install Chromium
|
||||
if [[ ${TRAVIS} && ${CI_MODE} == "js" || ${CI_MODE} == "e2e" || ${CI_MODE} == "e2e_2" || ${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e" ]]; then
|
||||
travisFoldStart "install-chromium"
|
||||
(
|
||||
${thisDir}/install-chromium.sh
|
||||
|
||||
# Start xvfb for local Chrome used for testing
|
||||
if [[ ${TRAVIS} ]]; then
|
||||
travisFoldStart "install-chromium.xvfb-start"
|
||||
sh -e /etc/init.d/xvfb start
|
||||
travisFoldEnd "install-chromium.xvfb-start"
|
||||
fi
|
||||
)
|
||||
travisFoldEnd "install-chromium"
|
||||
fi
|
||||
|
||||
|
||||
@ -92,12 +102,6 @@ if [[ ${TRAVIS} && (${CI_MODE} == "browserstack_required" || ${CI_MODE} == "brow
|
||||
fi
|
||||
|
||||
|
||||
# Install Selenium WebDriver
|
||||
travisFoldStart "webdriver-manager-update"
|
||||
$(npm bin)/webdriver-manager update
|
||||
travisFoldEnd "webdriver-manager-update"
|
||||
|
||||
|
||||
# Install bower packages
|
||||
travisFoldStart "bower-install"
|
||||
$(npm bin)/bower install
|
||||
|
Reference in New Issue
Block a user