Skip to content

Commit 1740085

Browse files
author
Emanuel Tesar
committed
feat(web): Make the integration more secure, fix tests
1 parent 7a443c5 commit 1740085

File tree

6 files changed

+152
-25
lines changed

6 files changed

+152
-25
lines changed

src/core/config.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export type Config = {
3333

3434
// legacy
3535
_lifecycleHooks: Array<string>;
36+
37+
// trusted types (https://github.com/WICG/trusted-types)
38+
trustedTypesPolicyName: string;
3639
};
3740

3841
export default ({
@@ -126,5 +129,11 @@ export default ({
126129
/**
127130
* Exposed for legacy reasons
128131
*/
129-
_lifecycleHooks: LIFECYCLE_HOOKS
132+
_lifecycleHooks: LIFECYCLE_HOOKS,
133+
134+
/**
135+
* Trusted Types policy name which will be used by Vue. More
136+
* info about Trusted Types on https://github.com/WICG/trusted-types.
137+
*/
138+
trustedTypesPolicyName: 'vue'
130139
}: Config)

src/platforms/web/runtime/modules/dom-props.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { isDef, isUndef, extend, toNumber } from 'shared/util'
44
import { isSVG } from 'web/util/index'
5-
import {convertToTrustedType} from 'web/security'
5+
import {maybeCreateDangerousSvgHTML} from 'web/security'
66

77
let svgContainer
88

@@ -53,7 +53,7 @@ function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
5353
} else if (key === 'innerHTML' && isSVG(elm.tagName) && isUndef(elm.innerHTML)) {
5454
// IE doesn't support innerHTML for SVG elements
5555
svgContainer = svgContainer || document.createElement('div')
56-
svgContainer.innerHTML = convertToTrustedType(`<svg>${cur}</svg>`)
56+
svgContainer.innerHTML = maybeCreateDangerousSvgHTML(cur)
5757
const svg = svgContainer.firstChild
5858
while (elm.firstChild) {
5959
elm.removeChild(elm.firstChild)

src/platforms/web/security.js

+17-9
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
/* @flow */
2+
import Vue from 'core/index'
3+
import {getTrustedTypes, isTrustedValue} from 'shared/util'
24

35
type TrustedTypePolicy = {
46
// value returned is actually an object with toString method returning the wrapped value
57
createHTML: (value: any) => string;
68
};
79

8-
let policy: TrustedTypePolicy
9-
export function convertToTrustedType(value: any) {
10+
let policy: ?TrustedTypePolicy
11+
// we need this function to clear the policy in tests
12+
Vue.prototype.$clearTrustedTypesPolicy = function() {
13+
policy = undefined
14+
}
15+
16+
export function maybeCreateDangerousSvgHTML(value: any): string {
1017
// create policy lazily to simplify testing
1118
const tt = getTrustedTypes()
1219
if (tt && !policy) {
13-
policy = tt.createPolicy('vue', {createHTML: (s) => s});
20+
policy = tt.createPolicy(Vue.config.trustedTypesPolicyName, {createHTML: (s) => s});
1421
}
1522

16-
if (!tt) return value;
17-
else return policy.createHTML(value);
23+
if (!tt) return `<svg>${value}</svg>`;
24+
else if (!isTrustedValue(value)) throw new Error('Expected svg innerHTML to be TrustedHTML!');
25+
// flow complains 'policy' may be undefined
26+
else return (policy: any).createHTML(`<svg>${value}</svg>`);
1827
}
1928

20-
export function getTrustedTypes() {
21-
// TrustedTypes have been renamed to trustedTypes https://github.com/WICG/trusted-types/issues/177
22-
return window.trustedTypes || window.TrustedTypes;
23-
}
29+
export function getTrustedShouldDecodeInnerHTML(href: boolean): string {
30+
return href ? `<a href="\n"/>` : `<div a="\n"/>`
31+
}

src/platforms/web/util/compat.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/* @flow */
22

33
import { inBrowser } from 'core/util/index'
4-
import {convertToTrustedType} from 'web/security'
4+
import {getTrustedShouldDecodeInnerHTML} from 'web/security'
55

66
// check whether current browser encodes a char inside attribute values
77
let div
88
function getShouldDecode (href: boolean): boolean {
99
div = div || document.createElement('div')
10-
div.innerHTML = convertToTrustedType(href ? `<a href="\n"/>` : `<div a="\n"/>`)
10+
div.innerHTML = getTrustedShouldDecodeInnerHTML(href)
1111
return div.innerHTML.indexOf('&#10;') > 0
1212
}
1313

src/shared/util.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,23 @@ export function isPromise (val: any): boolean {
7979
)
8080
}
8181

82+
export function getTrustedTypes() {
83+
// TrustedTypes have been renamed to trustedTypes https://github.com/WICG/trusted-types/issues/177
84+
return typeof window !== 'undefined' && (window.trustedTypes || window.TrustedTypes);
85+
}
86+
87+
export function isTrustedValue(value: any): boolean {
88+
const tt = getTrustedTypes();
89+
if (!tt) return false;
90+
// TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204
91+
else return tt.isHTML(value) || tt.isScript(value) || tt.isScriptURL(value) || (tt.isURL && tt.isURL(value))
92+
}
93+
8294
/**
8395
* Convert a value to a string that is actually rendered.
8496
*/
8597
export function toString (val: any): string {
86-
// TrustedTypes have been renamed to trustedTypes https://github.com/WICG/trusted-types/issues/177
87-
const tt = window.trustedTypes || window.TrustedTypes;
88-
// TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204
89-
if (tt && (tt.isHTML(val) || tt.isScript(val) || tt.isScriptURL(val) || (tt.isURL && tt.isURL(val)))) {
98+
if (isTrustedValue(val)) {
9099
return val;
91100
} else {
92101
return val == null

test/unit/features/trusted-types.spec.js

+108-7
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111
import Vue from 'vue'
1212

1313
// we don't differentiate between different types of trusted values
14-
const createTrustedValue = (value) => `TRUSTED${value}`;
15-
const isTrustedValue = (value) => value.startsWith('TRUSTED');
16-
const unwrapTrustedValue = (value) => value.substr('TRUSTED'.length);
14+
const createTrustedValue = (value) => ({toString: () => value, isTrusted: true})
15+
const isTrustedValue = (value) => value && value.isTrusted
1716

1817
const unsafeHtml = '<img src=x onerror="alert(0)">';
1918
const unsafeScript = 'alert(0)';
@@ -27,6 +26,7 @@ describe('rendering with trusted types enforced', () => {
2726
// thrown by unsafe setAttribute call (e.g. srcdoc in iframe) the rendering fails completely.
2827
// We log the errors, before throwing so we can be sure that trusted types work.
2928
let errorLog;
29+
let vuePolicyName;
3030

3131
function emulateSetAttribute() {
3232
// enforce trusted values only on properties in this array
@@ -38,7 +38,7 @@ describe('rendering with trusted types enforced', () => {
3838
unsafeAttributeList.forEach((attr) => {
3939
if (attr === name) {
4040
if (isTrustedValue(value)) {
41-
args = [name, unwrapTrustedValue(value)];
41+
args = [name, value.toString()];
4242
} else {
4343
errorLog.push(createTTErrorMessage(attr, value));
4444
throw new Error(value);
@@ -55,8 +55,9 @@ describe('rendering with trusted types enforced', () => {
5555
descriptorEntries.push({object, prop, desc});
5656
Object.defineProperty(object, prop, {
5757
set: function(value) {
58+
console.log('set', value, prop);
5859
if (isTrustedValue(value)) {
59-
desc.set.apply(this, [unwrapTrustedValue(value)]);
60+
desc.set.apply(this, [value.toString()]);
6061
} else {
6162
errorLog.push(createTTErrorMessage(prop, value));
6263
throw new Error(value);
@@ -81,7 +82,12 @@ describe('rendering with trusted types enforced', () => {
8182

8283
beforeEach(() => {
8384
window.trustedTypes = {
84-
createPolicy: () => {
85+
createPolicy: (name) => {
86+
// capture the name of the vue policy so we can test it. Relies on fact
87+
// that there are only 2 policies (for vue and for tests).
88+
if (name !== 'test-policy') {
89+
vuePolicyName = name;
90+
}
8591
return {
8692
createHTML: createTrustedValue,
8793
createScript: createTrustedValue,
@@ -98,9 +104,10 @@ describe('rendering with trusted types enforced', () => {
98104
emulateSetAttribute();
99105

100106
// TODO: this needs to be changed once we use trusted types polyfill
101-
policy = window.trustedTypes.createPolicy();
107+
policy = window.trustedTypes.createPolicy('test-policy');
102108

103109
errorLog = [];
110+
vuePolicyName = '';
104111
});
105112

106113
afterEach(() => {
@@ -119,6 +126,100 @@ describe('rendering with trusted types enforced', () => {
119126
}).toThrow();
120127
});
121128

129+
describe('vue policy', () => {
130+
let innerHTMLDescriptor;
131+
132+
// simulate svg elements in Internet Explorer which don't have 'innerHTML' property
133+
beforeEach(() => {
134+
innerHTMLDescriptor = Object.getOwnPropertyDescriptor(
135+
Element.prototype,
136+
'innerHTML',
137+
);
138+
delete Element.prototype.innerHTML;
139+
Object.defineProperty(
140+
HTMLDivElement.prototype,
141+
'innerHTML',
142+
innerHTMLDescriptor,
143+
);
144+
});
145+
146+
afterEach(() => {
147+
Vue.prototype.$clearTrustedTypesPolicy();
148+
149+
delete HTMLDivElement.prototype.innerHTML;
150+
Object.defineProperty(
151+
Element.prototype,
152+
'innerHTML',
153+
innerHTMLDescriptor,
154+
);
155+
});
156+
157+
it('uses default policy name "vue"', () => {
158+
// we need to trigger creation of vue policy
159+
const vm = new Vue({
160+
render: (c) => {
161+
return c('svg', {
162+
domProps: {
163+
innerHTML: policy.createHTML('safe html'),
164+
},
165+
});
166+
}
167+
})
168+
169+
vm.$mount();
170+
expect(vuePolicyName).toBe('vue');
171+
});
172+
173+
it('policy name can be configured', () => {
174+
Vue.config.trustedTypesPolicyName = 'userProvidedPolicyName';
175+
176+
// we need to trigger creation of vue policy
177+
const vm = new Vue({
178+
render: (c) => {
179+
return c('svg', {
180+
domProps: {
181+
innerHTML: policy.createHTML('safe html'),
182+
},
183+
});
184+
}
185+
})
186+
187+
vm.$mount();
188+
expect(vuePolicyName).toBe('userProvidedPolicyName');
189+
});
190+
191+
it('will throw an error on untrusted html', () => {
192+
const vm = new Vue({
193+
render: (c) => {
194+
return c('svg', {
195+
domProps: {
196+
innerHTML: unsafeHtml,
197+
},
198+
});
199+
}
200+
})
201+
202+
expect(() => {
203+
vm.$mount();
204+
}).toThrowError('Expected svg innerHTML to be TrustedHTML!');
205+
});
206+
207+
it('passes if payload is TrustedHTML', () => {
208+
const vm = new Vue({
209+
render: (c) => {
210+
return c('svg', {
211+
domProps: {
212+
innerHTML: policy.createHTML('safe html'),
213+
},
214+
});
215+
}
216+
})
217+
218+
vm.$mount();
219+
expect(vm.$el.textContent).toBe('safe html');
220+
});
221+
});
222+
122223
// html interpolation is safe because it's put into DOM as text node
123224
it('interpolation is trusted', () => {
124225
const vm = new Vue({

0 commit comments

Comments
 (0)