diff --git a/.travis.yml b/.travis.yml index 9d7d2526d7440..b71170795be8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6da0cb25d17c7..9aa324f89b7a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + +## [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)) + + + ## [4.4.6](https://github.com/angular/angular/compare/4.4.5...4.4.6) (2017-10-18) diff --git a/aio/karma.conf.js b/aio/karma.conf.js index c2d83c7804636..032dd4145363f 100644 --- a/aio/karma.conf.js +++ b/aio/karma.conf.js @@ -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'] + } + } }); }; diff --git a/aio/package.json b/aio/package.json index 0431e3d63402d..c575a860d5488 100644 --- a/aio/package.json +++ b/aio/package.json @@ -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", diff --git a/aio/protractor.conf.js b/aio/protractor.conf.js index 1cd695135f7e2..d112804b62c95 100644 --- a/aio/protractor.conf.js +++ b/aio/protractor.conf.js @@ -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, diff --git a/aio/scripts/test-pwa-score.js b/aio/scripts/test-pwa-score.js index a53e1b28b26d0..bf2fc2916a165 100644 --- a/aio/scripts/test-pwa-score.js +++ b/aio/scripts/test-pwa-score.js @@ -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)). diff --git a/aio/tools/examples/shared/package.json b/aio/tools/examples/shared/package.json index 85ba989112e2f..9c665fc06dc81 100644 --- a/aio/tools/examples/shared/package.json +++ b/aio/tools/examples/shared/package.json @@ -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": [], diff --git a/aio/tools/examples/shared/protractor.config.js b/aio/tools/examples/shared/protractor.config.js index 21b54feb8bc4b..4e97cc1d6ae72 100644 --- a/aio/tools/examples/shared/protractor.config.js +++ b/aio/tools/examples/shared/protractor.config.js @@ -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. diff --git a/integration/hello_world__closure/package.json b/integration/hello_world__closure/package.json index 51ec7029bace5..78519201cc869 100644 --- a/integration/hello_world__closure/package.json +++ b/integration/hello_world__closure/package.json @@ -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", diff --git a/integration/hello_world__systemjs_umd/package.json b/integration/hello_world__systemjs_umd/package.json index c3b3fb42b2a1d..bc65edc9bc7ff 100644 --- a/integration/hello_world__systemjs_umd/package.json +++ b/integration/hello_world__systemjs_umd/package.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", diff --git a/package.json b/package.json index e99953aacbe74..4d8d3a90fe505 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/packages/platform-browser/src/security/html_sanitizer.ts b/packages/platform-browser/src/security/html_sanitizer.ts index c0cccf3530984..7365df8813806 100644 --- a/packages/platform-browser/src/security/html_sanitizer.ts +++ b/packages/platform-browser/src/security/html_sanitizer.ts @@ -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
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 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); + } + } } } diff --git a/packages/platform-browser/src/security/inert_body.ts b/packages/platform-browser/src/security/inert_body.ts new file mode 100644 index 0000000000000..d4cb58fb45ba4 --- /dev/null +++ b/packages/platform-browser/src/security/inert_body.ts @@ -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, ''); + 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, '