Skip to content

Commit 7a443c5

Browse files
author
Emanuel Tesar
committed
feat(web): Integrate trusted types into Vue
1 parent 399b536 commit 7a443c5

File tree

5 files changed

+259
-7
lines changed

5 files changed

+259
-7
lines changed

Diff for: src/platforms/web/runtime/modules/dom-props.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +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'
56

67
let svgContainer
78

@@ -20,6 +21,7 @@ function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
2021

2122
for (key in oldProps) {
2223
if (!(key in props)) {
24+
// TT_TODO: when (how) is this even called
2325
elm[key] = ''
2426
}
2527
}
@@ -51,7 +53,7 @@ function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
5153
} else if (key === 'innerHTML' && isSVG(elm.tagName) && isUndef(elm.innerHTML)) {
5254
// IE doesn't support innerHTML for SVG elements
5355
svgContainer = svgContainer || document.createElement('div')
54-
svgContainer.innerHTML = `<svg>${cur}</svg>`
56+
svgContainer.innerHTML = convertToTrustedType(`<svg>${cur}</svg>`)
5557
const svg = svgContainer.firstChild
5658
while (elm.firstChild) {
5759
elm.removeChild(elm.firstChild)

Diff for: src/platforms/web/security.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* @flow */
2+
3+
type TrustedTypePolicy = {
4+
// value returned is actually an object with toString method returning the wrapped value
5+
createHTML: (value: any) => string;
6+
};
7+
8+
let policy: TrustedTypePolicy
9+
export function convertToTrustedType(value: any) {
10+
// create policy lazily to simplify testing
11+
const tt = getTrustedTypes()
12+
if (tt && !policy) {
13+
policy = tt.createPolicy('vue', {createHTML: (s) => s});
14+
}
15+
16+
if (!tt) return value;
17+
else return policy.createHTML(value);
18+
}
19+
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+
}

Diff for: src/platforms/web/util/compat.js

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

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

56
// check whether current browser encodes a char inside attribute values
67
let div
78
function getShouldDecode (href: boolean): boolean {
89
div = div || document.createElement('div')
9-
div.innerHTML = href ? `<a href="\n"/>` : `<div a="\n"/>`
10+
div.innerHTML = convertToTrustedType(href ? `<a href="\n"/>` : `<div a="\n"/>`)
1011
return div.innerHTML.indexOf('&#10;') > 0
1112
}
1213

Diff for: src/shared/util.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,18 @@ export function isPromise (val: any): boolean {
8383
* Convert a value to a string that is actually rendered.
8484
*/
8585
export function toString (val: any): string {
86-
return val == null
87-
? ''
88-
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
89-
? JSON.stringify(val, null, 2)
90-
: String(val)
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)))) {
90+
return val;
91+
} else {
92+
return val == null
93+
? ''
94+
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
95+
? JSON.stringify(val, null, 2)
96+
: String(val)
97+
}
9198
}
9299

93100
/**

Diff for: test/unit/features/trusted-types.spec.js

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// NOTE: We emulate trusted types behaviour such that the tests
2+
// are deterministic. These tests needs to be updated if the trusted
3+
// types API changes.
4+
//
5+
// You can find trusted types repository here:
6+
// https://github.com/WICG/trusted-types
7+
//
8+
// TODO: replace testing setup with polyfill, once it exports
9+
// enforcing API.
10+
11+
import Vue from 'vue'
12+
13+
// 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);
17+
18+
const unsafeHtml = '<img src=x onerror="alert(0)">';
19+
const unsafeScript = 'alert(0)';
20+
21+
describe('rendering with trusted types enforced', () => {
22+
let descriptorEntries = [];
23+
let setAttributeDescriptor;
24+
let policy;
25+
// NOTE: trusted type error is not propagated from v-html directive and application will not
26+
// render the dangerous html, but will continue rendering other components. If the error is
27+
// thrown by unsafe setAttribute call (e.g. srcdoc in iframe) the rendering fails completely.
28+
// We log the errors, before throwing so we can be sure that trusted types work.
29+
let errorLog;
30+
31+
function emulateSetAttribute() {
32+
// enforce trusted values only on properties in this array
33+
const unsafeAttributeList = ['srcdoc', 'onclick'];
34+
setAttributeDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'setAttribute');
35+
Object.defineProperty(Element.prototype, 'setAttribute', {
36+
value: function(name, value) {
37+
let args = [name, value];
38+
unsafeAttributeList.forEach((attr) => {
39+
if (attr === name) {
40+
if (isTrustedValue(value)) {
41+
args = [name, unwrapTrustedValue(value)];
42+
} else {
43+
errorLog.push(createTTErrorMessage(attr, value));
44+
throw new Error(value);
45+
}
46+
}
47+
});
48+
setAttributeDescriptor.value.apply(this, args);
49+
}
50+
});
51+
}
52+
53+
function emulateTrustedTypesOnProperty(object, prop) {
54+
const desc = Object.getOwnPropertyDescriptor(object, prop);
55+
descriptorEntries.push({object, prop, desc});
56+
Object.defineProperty(object, prop, {
57+
set: function(value) {
58+
if (isTrustedValue(value)) {
59+
desc.set.apply(this, [unwrapTrustedValue(value)]);
60+
} else {
61+
errorLog.push(createTTErrorMessage(prop, value));
62+
throw new Error(value);
63+
}
64+
},
65+
});
66+
}
67+
68+
function removeAllTrustedTypesEmulation() {
69+
descriptorEntries.forEach(({object, prop, desc}) => {
70+
Object.defineProperty(object, prop, desc);
71+
});
72+
descriptorEntries = [];
73+
74+
Object.defineProperty(
75+
Element.prototype, 'setAttribute', setAttributeDescriptor);
76+
}
77+
78+
function createTTErrorMessage(name, value) {
79+
return `TT ERROR: ${name} ${value}`;
80+
}
81+
82+
beforeEach(() => {
83+
window.trustedTypes = {
84+
createPolicy: () => {
85+
return {
86+
createHTML: createTrustedValue,
87+
createScript: createTrustedValue,
88+
createScriptURL: createTrustedValue,
89+
};
90+
},
91+
isHTML: (v) => isTrustedValue(v),
92+
isScript: (v) => isTrustedValue(v),
93+
isScriptURL: (v) => isTrustedValue(v),
94+
};
95+
96+
emulateTrustedTypesOnProperty(Element.prototype, 'innerHTML');
97+
emulateTrustedTypesOnProperty(HTMLIFrameElement.prototype, 'srcdoc');
98+
emulateSetAttribute();
99+
100+
// TODO: this needs to be changed once we use trusted types polyfill
101+
policy = window.trustedTypes.createPolicy();
102+
103+
errorLog = [];
104+
});
105+
106+
afterEach(() => {
107+
removeAllTrustedTypesEmulation();
108+
delete window.trustedTypes;
109+
});
110+
111+
it('Trusted types emulation works', () => {
112+
const el = document.createElement('div');
113+
expect(el.innerHTML).toBe('');
114+
el.innerHTML = policy.createHTML('<span>val</span>');
115+
expect(el.innerHTML, '<span>val</span>');
116+
117+
expect(() => {
118+
el.innerHTML = '<span>val</span>';
119+
}).toThrow();
120+
});
121+
122+
// html interpolation is safe because it's put into DOM as text node
123+
it('interpolation is trusted', () => {
124+
const vm = new Vue({
125+
data: {
126+
unsafeHtml,
127+
},
128+
template: '<div>{{unsafeHtml}}</div>'
129+
})
130+
131+
vm.$mount();
132+
expect(vm.$el.textContent).toBe(document.createTextNode(unsafeHtml).textContent);
133+
});
134+
135+
describe('throws on untrusted values', () => {
136+
it('v-html directive', () => {
137+
const vm = new Vue({
138+
data: {
139+
unsafeHtml,
140+
},
141+
template: '<div v-html="unsafeHtml"></div>'
142+
})
143+
144+
vm.$mount();
145+
expect(errorLog).toEqual([createTTErrorMessage('innerHTML', unsafeHtml)]);
146+
});
147+
148+
it('attribute interpolation', () => {
149+
const vm = new Vue({
150+
data: {
151+
unsafeHtml,
152+
},
153+
template: '<iframe :srcdoc="unsafeHtml"></iframe>'
154+
})
155+
156+
expect(() => {
157+
vm.$mount();
158+
}).toThrow();
159+
expect(errorLog).toEqual([createTTErrorMessage('srcdoc', unsafeHtml)]);
160+
});
161+
162+
it('on* events', () => {
163+
const vm = new Vue({
164+
data: {
165+
unsafeScript,
166+
},
167+
template: '<button :onclick="unsafeScript">unsafe btn</button>'
168+
})
169+
170+
expect(() => {
171+
vm.$mount();
172+
}).toThrow();
173+
expect(errorLog).toEqual([createTTErrorMessage('onclick', unsafeScript)]);
174+
});
175+
});
176+
177+
describe('runs without error on trusted values', () => {
178+
it('v-html directive', () => {
179+
const vm = new Vue({
180+
data: {
181+
safeHtml: policy.createHTML('safeHtmlValue'),
182+
},
183+
template: '<div v-html="safeHtml"></div>'
184+
})
185+
186+
vm.$mount();
187+
expect(vm.$el.innerHTML).toBe('safeHtmlValue');
188+
expect(errorLog).toEqual([]);
189+
});
190+
191+
it('attribute interpolation', () => {
192+
const vm = new Vue({
193+
data: {
194+
safeScript: policy.createScript('safeScriptValue'),
195+
},
196+
template: '<iframe :srcdoc="safeScript"></iframe>'
197+
})
198+
199+
vm.$mount();
200+
expect(vm.$el.srcdoc).toBe('safeScriptValue');
201+
expect(errorLog).toEqual([]);
202+
});
203+
204+
it('on* events', () => {
205+
const vm = new Vue({
206+
data: {
207+
safeScript: policy.createScript('safeScriptValue'),
208+
},
209+
template: '<button :onclick="safeScript">unsafe btn</button>'
210+
})
211+
212+
vm.$mount();
213+
const onClickFn = vm.$el.onclick.toString();
214+
const onClickBody = onClickFn.substring(onClickFn.indexOf("{") + 1, onClickFn.lastIndexOf("}"));
215+
expect(onClickBody.trim()).toBe('safeScriptValue');
216+
expect(errorLog).toEqual([]);
217+
});
218+
});
219+
});

0 commit comments

Comments
 (0)