Skip to content

Commit

Permalink
feat(web): Make the integration more secure, fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Emanuel Tesar committed Sep 9, 2019
1 parent 7a443c5 commit 1740085
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 25 deletions.
11 changes: 10 additions & 1 deletion src/core/config.js
Expand Up @@ -33,6 +33,9 @@ export type Config = {

// legacy
_lifecycleHooks: Array<string>;

// trusted types (https://github.com/WICG/trusted-types)
trustedTypesPolicyName: string;
};

export default ({
Expand Down Expand Up @@ -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)
4 changes: 2 additions & 2 deletions src/platforms/web/runtime/modules/dom-props.js
Expand Up @@ -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

Expand Down Expand Up @@ -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(`<svg>${cur}</svg>`)
svgContainer.innerHTML = maybeCreateDangerousSvgHTML(cur)
const svg = svgContainer.firstChild
while (elm.firstChild) {
elm.removeChild(elm.firstChild)
Expand Down
26 changes: 17 additions & 9 deletions 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 `<svg>${value}</svg>`;
else if (!isTrustedValue(value)) throw new Error('Expected svg innerHTML to be TrustedHTML!');
// flow complains 'policy' may be undefined
else return (policy: any).createHTML(`<svg>${value}</svg>`);
}

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 ? `<a href="\n"/>` : `<div a="\n"/>`
}
4 changes: 2 additions & 2 deletions 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 ? `<a href="\n"/>` : `<div a="\n"/>`)
div.innerHTML = getTrustedShouldDecodeInnerHTML(href)
return div.innerHTML.indexOf('&#10;') > 0
}

Expand Down
17 changes: 13 additions & 4 deletions src/shared/util.js
Expand Up @@ -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
Expand Down
115 changes: 108 additions & 7 deletions test/unit/features/trusted-types.spec.js
Expand Up @@ -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 = '<img src=x onerror="alert(0)">';
const unsafeScript = 'alert(0)';
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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,
Expand All @@ -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(() => {
Expand All @@ -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({
Expand Down

0 comments on commit 1740085

Please sign in to comment.