From 1740085cfdadae4444711a5e3a716ee77f7ba1a7 Mon Sep 17 00:00:00 2001 From: Emanuel Tesar Date: Fri, 6 Sep 2019 17:31:22 +0200 Subject: [PATCH] feat(web): Make the integration more secure, fix tests --- src/core/config.js | 11 +- .../web/runtime/modules/dom-props.js | 4 +- src/platforms/web/security.js | 26 ++-- src/platforms/web/util/compat.js | 4 +- src/shared/util.js | 17 ++- test/unit/features/trusted-types.spec.js | 115 ++++++++++++++++-- 6 files changed, 152 insertions(+), 25 deletions(-) diff --git a/src/core/config.js b/src/core/config.js index cae9d3dd395..039dbb5c8ae 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -33,6 +33,9 @@ export type Config = { // legacy _lifecycleHooks: Array; + + // trusted types (https://github.com/WICG/trusted-types) + trustedTypesPolicyName: string; }; export default ({ @@ -126,5 +129,11 @@ export default ({ /** * Exposed for legacy reasons */ - _lifecycleHooks: LIFECYCLE_HOOKS + _lifecycleHooks: LIFECYCLE_HOOKS, + + /** + * Trusted Types policy name which will be used by Vue. More + * info about Trusted Types on https://github.com/WICG/trusted-types. + */ + trustedTypesPolicyName: 'vue' }: Config) diff --git a/src/platforms/web/runtime/modules/dom-props.js b/src/platforms/web/runtime/modules/dom-props.js index 4297151fab2..00cf182674f 100644 --- a/src/platforms/web/runtime/modules/dom-props.js +++ b/src/platforms/web/runtime/modules/dom-props.js @@ -2,7 +2,7 @@ import { isDef, isUndef, extend, toNumber } from 'shared/util' import { isSVG } from 'web/util/index' -import {convertToTrustedType} from 'web/security' +import {maybeCreateDangerousSvgHTML} from 'web/security' let svgContainer @@ -53,7 +53,7 @@ function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) { } else if (key === 'innerHTML' && isSVG(elm.tagName) && isUndef(elm.innerHTML)) { // IE doesn't support innerHTML for SVG elements svgContainer = svgContainer || document.createElement('div') - svgContainer.innerHTML = convertToTrustedType(`${cur}`) + svgContainer.innerHTML = maybeCreateDangerousSvgHTML(cur) const svg = svgContainer.firstChild while (elm.firstChild) { elm.removeChild(elm.firstChild) diff --git a/src/platforms/web/security.js b/src/platforms/web/security.js index dffcd76354b..fff83f2ed33 100644 --- a/src/platforms/web/security.js +++ b/src/platforms/web/security.js @@ -1,23 +1,31 @@ /* @flow */ +import Vue from 'core/index' +import {getTrustedTypes, isTrustedValue} from 'shared/util' type TrustedTypePolicy = { // value returned is actually an object with toString method returning the wrapped value createHTML: (value: any) => string; }; -let policy: TrustedTypePolicy -export function convertToTrustedType(value: any) { +let policy: ?TrustedTypePolicy +// we need this function to clear the policy in tests +Vue.prototype.$clearTrustedTypesPolicy = function() { + policy = undefined +} + +export function maybeCreateDangerousSvgHTML(value: any): string { // create policy lazily to simplify testing const tt = getTrustedTypes() if (tt && !policy) { - policy = tt.createPolicy('vue', {createHTML: (s) => s}); + policy = tt.createPolicy(Vue.config.trustedTypesPolicyName, {createHTML: (s) => s}); } - if (!tt) return value; - else return policy.createHTML(value); + if (!tt) return `${value}`; + else if (!isTrustedValue(value)) throw new Error('Expected svg innerHTML to be TrustedHTML!'); + // flow complains 'policy' may be undefined + else return (policy: any).createHTML(`${value}`); } -export function getTrustedTypes() { - // TrustedTypes have been renamed to trustedTypes https://github.com/WICG/trusted-types/issues/177 - return window.trustedTypes || window.TrustedTypes; -} +export function getTrustedShouldDecodeInnerHTML(href: boolean): string { + return href ? `` : `
` +} \ No newline at end of file diff --git a/src/platforms/web/util/compat.js b/src/platforms/web/util/compat.js index 8c5b32ce8ad..a95339778a6 100644 --- a/src/platforms/web/util/compat.js +++ b/src/platforms/web/util/compat.js @@ -1,13 +1,13 @@ /* @flow */ import { inBrowser } from 'core/util/index' -import {convertToTrustedType} from 'web/security' +import {getTrustedShouldDecodeInnerHTML} from 'web/security' // check whether current browser encodes a char inside attribute values let div function getShouldDecode (href: boolean): boolean { div = div || document.createElement('div') - div.innerHTML = convertToTrustedType(href ? `` : `
`) + div.innerHTML = getTrustedShouldDecodeInnerHTML(href) return div.innerHTML.indexOf(' ') > 0 } diff --git a/src/shared/util.js b/src/shared/util.js index 7d76afd8115..b5aea3b3eba 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -79,14 +79,23 @@ export function isPromise (val: any): boolean { ) } +export function getTrustedTypes() { + // TrustedTypes have been renamed to trustedTypes https://github.com/WICG/trusted-types/issues/177 + return typeof window !== 'undefined' && (window.trustedTypes || window.TrustedTypes); +} + +export function isTrustedValue(value: any): boolean { + const tt = getTrustedTypes(); + if (!tt) return false; + // TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204 + else return tt.isHTML(value) || tt.isScript(value) || tt.isScriptURL(value) || (tt.isURL && tt.isURL(value)) +} + /** * Convert a value to a string that is actually rendered. */ export function toString (val: any): string { - // TrustedTypes have been renamed to trustedTypes https://github.com/WICG/trusted-types/issues/177 - const tt = window.trustedTypes || window.TrustedTypes; - // TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204 - if (tt && (tt.isHTML(val) || tt.isScript(val) || tt.isScriptURL(val) || (tt.isURL && tt.isURL(val)))) { + if (isTrustedValue(val)) { return val; } else { return val == null diff --git a/test/unit/features/trusted-types.spec.js b/test/unit/features/trusted-types.spec.js index eee96d54865..9b074b8cf5c 100644 --- a/test/unit/features/trusted-types.spec.js +++ b/test/unit/features/trusted-types.spec.js @@ -11,9 +11,8 @@ import Vue from 'vue' // we don't differentiate between different types of trusted values -const createTrustedValue = (value) => `TRUSTED${value}`; -const isTrustedValue = (value) => value.startsWith('TRUSTED'); -const unwrapTrustedValue = (value) => value.substr('TRUSTED'.length); +const createTrustedValue = (value) => ({toString: () => value, isTrusted: true}) +const isTrustedValue = (value) => value && value.isTrusted const unsafeHtml = ''; const unsafeScript = 'alert(0)'; @@ -27,6 +26,7 @@ describe('rendering with trusted types enforced', () => { // thrown by unsafe setAttribute call (e.g. srcdoc in iframe) the rendering fails completely. // We log the errors, before throwing so we can be sure that trusted types work. let errorLog; + let vuePolicyName; function emulateSetAttribute() { // enforce trusted values only on properties in this array @@ -38,7 +38,7 @@ describe('rendering with trusted types enforced', () => { unsafeAttributeList.forEach((attr) => { if (attr === name) { if (isTrustedValue(value)) { - args = [name, unwrapTrustedValue(value)]; + args = [name, value.toString()]; } else { errorLog.push(createTTErrorMessage(attr, value)); throw new Error(value); @@ -55,8 +55,9 @@ describe('rendering with trusted types enforced', () => { descriptorEntries.push({object, prop, desc}); Object.defineProperty(object, prop, { set: function(value) { + console.log('set', value, prop); if (isTrustedValue(value)) { - desc.set.apply(this, [unwrapTrustedValue(value)]); + desc.set.apply(this, [value.toString()]); } else { errorLog.push(createTTErrorMessage(prop, value)); throw new Error(value); @@ -81,7 +82,12 @@ describe('rendering with trusted types enforced', () => { beforeEach(() => { window.trustedTypes = { - createPolicy: () => { + createPolicy: (name) => { + // capture the name of the vue policy so we can test it. Relies on fact + // that there are only 2 policies (for vue and for tests). + if (name !== 'test-policy') { + vuePolicyName = name; + } return { createHTML: createTrustedValue, createScript: createTrustedValue, @@ -98,9 +104,10 @@ describe('rendering with trusted types enforced', () => { emulateSetAttribute(); // TODO: this needs to be changed once we use trusted types polyfill - policy = window.trustedTypes.createPolicy(); + policy = window.trustedTypes.createPolicy('test-policy'); errorLog = []; + vuePolicyName = ''; }); afterEach(() => { @@ -119,6 +126,100 @@ describe('rendering with trusted types enforced', () => { }).toThrow(); }); + describe('vue policy', () => { + let innerHTMLDescriptor; + + // simulate svg elements in Internet Explorer which don't have 'innerHTML' property + beforeEach(() => { + innerHTMLDescriptor = Object.getOwnPropertyDescriptor( + Element.prototype, + 'innerHTML', + ); + delete Element.prototype.innerHTML; + Object.defineProperty( + HTMLDivElement.prototype, + 'innerHTML', + innerHTMLDescriptor, + ); + }); + + afterEach(() => { + Vue.prototype.$clearTrustedTypesPolicy(); + + delete HTMLDivElement.prototype.innerHTML; + Object.defineProperty( + Element.prototype, + 'innerHTML', + innerHTMLDescriptor, + ); + }); + + it('uses default policy name "vue"', () => { + // we need to trigger creation of vue policy + const vm = new Vue({ + render: (c) => { + return c('svg', { + domProps: { + innerHTML: policy.createHTML('safe html'), + }, + }); + } + }) + + vm.$mount(); + expect(vuePolicyName).toBe('vue'); + }); + + it('policy name can be configured', () => { + Vue.config.trustedTypesPolicyName = 'userProvidedPolicyName'; + + // we need to trigger creation of vue policy + const vm = new Vue({ + render: (c) => { + return c('svg', { + domProps: { + innerHTML: policy.createHTML('safe html'), + }, + }); + } + }) + + vm.$mount(); + expect(vuePolicyName).toBe('userProvidedPolicyName'); + }); + + it('will throw an error on untrusted html', () => { + const vm = new Vue({ + render: (c) => { + return c('svg', { + domProps: { + innerHTML: unsafeHtml, + }, + }); + } + }) + + expect(() => { + vm.$mount(); + }).toThrowError('Expected svg innerHTML to be TrustedHTML!'); + }); + + it('passes if payload is TrustedHTML', () => { + const vm = new Vue({ + render: (c) => { + return c('svg', { + domProps: { + innerHTML: policy.createHTML('safe html'), + }, + }); + } + }) + + vm.$mount(); + expect(vm.$el.textContent).toBe('safe html'); + }); + }); + // html interpolation is safe because it's put into DOM as text node it('interpolation is trusted', () => { const vm = new Vue({