UNPKG

12.6 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright 2018 Google LLC
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18'use strict';
19
20require('module-keys/cjs').polyfill(module, require, 'web-contract-types');
21
22const { Mintable } = require('node-sec-patterns');
23const { TypedString } = require('template-tag-common');
24
25const { defineProperties, freeze } = Object;
26const { apply } = Reflect;
27const { exec: reExec, test: reTest } = RegExp.prototype;
28const { replace } = String.prototype;
29
30const { stringify: JSONstringify } = JSON;
31const { encodeURIComponent } = global;
32
33// eslint-disable-next-line no-control-regex
34const HTML_SPECIAL = /[\x00<>&"'+=@`{]/g;
35const HTML_ESCS = {
36 __proto__: null,
37 '\x00': '',
38 '<': '&lt;',
39 '>': '&gt;',
40 '&': '&amp;',
41 '"': '&#34;',
42 '\'': '&#39;',
43 // UTF-7
44 '+': '&#43;',
45 '=': '&#61;',
46 // Conditional compilation.
47 '@': '&#64;',
48 // IE non-standard attribute delimiter.
49 '`': '&#96;',
50 // Client-side templating.
51 '{': '&#123;',
52};
53function escapeHtmlMetaCharacter(chr) {
54 return HTML_ESCS[chr];
55}
56
57function htmlEscapeString(str) {
58 return apply(replace, `${ str }`, [ HTML_SPECIAL, escapeHtmlMetaCharacter ]);
59}
60
61const SCRIPT_OR_CDATA_END = /<\/script|\]\]>/i;
62
63
64class TrustedType extends TypedString {}
65defineProperties(
66 TrustedType.prototype,
67 {
68 'toJSON': {
69 // eslint-disable-next-line func-name-matching
70 value: function toJSON() {
71 // Pug templates depends on this particular behavior
72 // since pug.attr coerces object values to JSON internally.
73 return this.content;
74 },
75 },
76 });
77
78
79/**
80 * A string that is safe to use in HTML context in DOM APIs and HTML documents.
81 *
82 * A TrustedHTML is a string-like object that carries the security type contract
83 * that its value as a string will not cause untrusted script execution when
84 * evaluated as HTML in a browser.
85 *
86 * Values of this type are guaranteed to be safe to use in HTML contexts,
87 * such as, assignment to the innerHTML DOM property, or interpolation into
88 * a HTML template in HTML PC_DATA context, in the sense that the use will not
89 * result in a Cross-Site-Scripting vulnerability.
90 *
91 * Instances must be created by Mintable.minterFor(TrustedHTML).
92 *
93 * When checking types, use Mintable.verifierFor(TrustedHTML) and do not rely on
94 * {@code instanceof}.
95 */
96class TrustedHTML extends TrustedType {}
97
98
99/**
100 * A URL which is under application control and from which script, CSS, and
101 * other resources that represent executable code, can be fetched.
102 *
103 * Given that the URL can only be constructed from strings under application
104 * control and is used to load resources, bugs resulting in a malformed URL
105 * should not have a security impact and are likely to be easily detectable
106 * during testing. Given the wide number of non-RFC compliant URLs in use,
107 * stricter validation could prevent some applications from being able to use
108 * this type.
109 *
110 * Instances must be created by Mintable.minterFor(TrustedResourceURL).
111 *
112 * When checking types, use Mintable.verifierFor(TrustedResourceURL) and do
113 * not rely on {@code instanceof}.
114 */
115class TrustedResourceURL extends TrustedType {}
116
117
118/**
119 * A string-like object which represents JavaScript code and that carries the
120 * security type contract that its value, as a string, will not cause execution
121 * of unconstrained attacker controlled code (XSS) when evaluated as JavaScript
122 * in a browser.
123 *
124 * A TrustedScript's string representation can safely be interpolated as the
125 * content of a script element within HTML. The TrustedScript string should not be
126 * escaped before interpolation.
127 *
128 * Note that the TrustedScript might contain text that is attacker-controlled but
129 * that text should have been interpolated with appropriate escaping,
130 * sanitization and/or validation into the right location in the script, such
131 * that it is highly constrained in its effect (for example, it had to match a
132 * set of whitelisted words).
133 *
134 * Instances must be created by Mintable.minterFor(TrustedScript).
135 *
136 * When checking types, use Mintable.verifierFor(TrustedScript) and do
137 * not rely on {@code instanceof}.
138 */
139class TrustedScript extends TrustedType {}
140
141
142/**
143 * A string that is safe to use in URL context in DOM APIs and HTML documents.
144 *
145 * A TrustedURL is a string-like object that carries the security type contract
146 * that its value as a string will not cause untrusted script execution
147 * when evaluated as a hyperlink URL in a browser.
148 *
149 * Values of this type are guaranteed to be safe to use in URL/hyperlink
150 * contexts, such as assignment to URL-valued DOM properties, in the sense that
151 * the use will not result in a Cross-Site-Scripting vulnerability. Similarly,
152 * TrustedURLs can be interpolated into the URL context of an HTML template (e.g.,
153 * inside a href attribute). However, appropriate HTML-escaping must still be
154 * applied.
155 *
156 * Instances must be created by Mintable.minterFor(TrustedURL).
157 *
158 * When checking types, use Mintable.verifierFor(TrustedURL) and do not rely on
159 * {@code instanceof}.
160 */
161class TrustedURL extends TrustedType {}
162
163
164defineProperties(TrustedHTML, {
165 'contractKey': {
166 enumerable: true,
167 value: 'web-contract-types/TrustedHTML',
168 },
169});
170defineProperties(TrustedResourceURL, {
171 'contractKey': {
172 enumerable: true,
173 value: 'web-contract-types/TrustedResourceURL',
174 },
175});
176defineProperties(TrustedScript, {
177 'contractKey': {
178 enumerable: true,
179 value: 'web-contract-types/TrustedScript',
180 },
181});
182defineProperties(TrustedURL, {
183 'contractKey': {
184 enumerable: true,
185 value: 'web-contract-types/TrustedURL',
186 },
187});
188
189
190function minterFor(TrustedTypeT) {
191 let warned = false;
192 function singleWarningFallback(x) {
193 if (!warned) {
194 warned = true;
195 // eslint-disable-next-line no-console
196 console.warning(
197 `web-contract-types not authorized to create ${ TrustedTypeT.name
198 }. Maybe check your mintable grants used to initialize node-sec-patterns.`);
199 }
200 return `${ x }`;
201 }
202
203 return require.keys.unbox(
204 Mintable.minterFor(TrustedTypeT),
205 () => true,
206 singleWarningFallback);
207}
208
209
210const mintTrustedHTML = minterFor(TrustedHTML);
211const isTrustedHTML = Mintable.verifierFor(TrustedHTML);
212
213const mintTrustedResourceURL = minterFor(TrustedResourceURL);
214const isTrustedResourceURL = Mintable.verifierFor(TrustedResourceURL);
215
216const mintTrustedScript = minterFor(TrustedScript);
217const isTrustedScript = Mintable.verifierFor(TrustedScript);
218
219const mintTrustedURL = minterFor(TrustedURL);
220const isTrustedURL = Mintable.verifierFor(TrustedURL);
221
222
223defineProperties(
224 TrustedHTML,
225 {
226 'concat': {
227 enumerable: true,
228 // eslint-disable-next-line func-name-matching
229 value: function concat(...els) {
230 let content = '';
231 for (const element of els) {
232 if (!isTrustedHTML(element)) {
233 throw new TypeError(`Expected TrustedHTML not ${ element }`);
234 }
235 content += element.content;
236 }
237 return mintTrustedHTML(content);
238 },
239 },
240 'empty': {
241 enumerable: true,
242 value: freeze(mintTrustedHTML('')),
243 },
244 'escape': {
245 enumerable: true,
246 // eslint-disable-next-line func-name-matching
247 value: function escape(val) {
248 return (isTrustedHTML(val)) ? val : mintTrustedHTML(htmlEscapeString(val));
249 },
250 },
251 'fromScript': {
252 enumerable: true,
253 // eslint-disable-next-line func-name-matching
254 value: function fromScript(src, { nonce, type, async, defer } = {}) {
255 // Use a comment to prevent dangling markup attacks against nonces.
256 let html = '<!-- --><script';
257 if (nonce) {
258 html += ` nonce="${ htmlEscapeString(nonce) }"`;
259 }
260 if (type) {
261 html += ` type="${ htmlEscapeString(type) }"`;
262 }
263 if (async) {
264 html += ' async="async"';
265 }
266 if (defer) {
267 html += ' defer="defer"';
268 }
269 if (isTrustedResourceURL(src)) {
270 html += ` src="${ htmlEscapeString(src.content) }">`;
271 } else if (isTrustedScript(src)) {
272 const { content } = src;
273 if (apply(reTest, SCRIPT_OR_CDATA_END, [ content ])) {
274 throw new Error(`TrustedScript is not embeddable in HTML ${ content }`);
275 }
276 // Use CDATA to avoid mismatches in foreign content parsing in <svg>.
277 html += `>//<![CDATA[\n${ content }\n//]]>`;
278 } else {
279 throw new TypeError('Expected either a TrustedResourceURL or a TrustedScript for src');
280 }
281 html += '</script>';
282 return mintTrustedHTML(html);
283 },
284 },
285 'is': {
286 enumerable: true,
287 value: isTrustedHTML,
288 },
289 });
290
291const innocuousResourceURL = freeze(mintTrustedURL('about:invalid#TrustedResourceURL'));
292
293defineProperties(
294 TrustedResourceURL,
295 {
296 'fromScript': {
297 enumerable: true,
298 // eslint-disable-next-line func-name-matching
299 value: function fromScript(script) {
300 if (isTrustedScript(script)) {
301 return mintTrustedResourceURL(
302 // UTF-8 since that's the output encoding used by encodeURIComponent
303 // Hash at the ends makes it more concatenation safe.
304 `data:text/javascript;charset=UTF-8,${ encodeURIComponent(script.content) }#`);
305 }
306 throw new TypeError('Expected TrustedScript');
307 },
308 },
309 'innocuousURL': {
310 enumerable: true,
311 value: innocuousResourceURL,
312 },
313 'is': {
314 enumerable: true,
315 value: isTrustedResourceURL,
316 },
317 });
318
319const LS_GLOBAL = /\u2028/g;
320const PS_GLOBAL = /\u2029/g;
321
322defineProperties(
323 TrustedScript,
324 {
325 'expressionFromJSON': {
326 enumerable: true,
327 // eslint-disable-next-line func-name-matching
328 value: function expressionFromJSON(...args) {
329 const json = JSONstringify(...args);
330 // JSON is not a subset of JS because of these two LineTerminator codepoints.
331 let javascript = apply(replace, json, [ LS_GLOBAL, '\\u2028' ]);
332 javascript = apply(replace, javascript, [ PS_GLOBAL, '\\u2029' ]);
333 return mintTrustedScript(`(${ javascript })`);
334 },
335 },
336 'innocuousScript': {
337 enumerable: true,
338 value: freeze(mintTrustedScript('[null][0]/*TrustedScript*/')),
339 },
340 'is': {
341 enumerable: true,
342 value: isTrustedScript,
343 },
344 });
345
346// Whitespace followed by a scheme, followed by a scheme specific part, followed by trailing whitespace.
347const SCHEME_AND_REST = /^[\t\n\f\r ]*([^/:?#]+:)?([\s\S]*?)[\t\n\f\r ]*$/;
348// URL schemes that we allow in arbitrary strings.
349// This list is kept intentionally short.
350// There may be URL schemes with well-understood security properties that are not on this list.
351// Use a minter if you need a TrustedURL that is not on this list.
352const SAFE_SCHEME_WHITELIST = {
353 __proto__: null,
354 'http:': true,
355 'https:': true,
356 'mailto:': true,
357 'tel:': true,
358};
359
360const innocuousURL = freeze(mintTrustedURL('about:invalid#TrustedURL'));
361
362defineProperties(
363 TrustedURL,
364 {
365 'innocuousURL': {
366 enumerable: true,
367 value: innocuousURL,
368 },
369 'sanitize': {
370 enumerable: true,
371 // eslint-disable-next-line func-name-matching
372 value: function sanitize(val, fallback = innocuousURL) {
373 if (isTrustedURL(val)) {
374 return val;
375 }
376 if (isTrustedResourceURL(val)) {
377 return mintTrustedURL(val);
378 }
379 const str = `${ val }`;
380 const [ , scheme, schemeSpecificPart ] = apply(reExec, SCHEME_AND_REST, [ str ]);
381 if (!scheme) {
382 return mintTrustedURL(schemeSpecificPart);
383 }
384 const canonScheme = scheme.toLowerCase();
385 if (SAFE_SCHEME_WHITELIST[canonScheme]) {
386 return mintTrustedURL(`${ canonScheme }${ schemeSpecificPart }`);
387 }
388 return fallback;
389 },
390 },
391 'is': {
392 enumerable: true,
393 value: isTrustedURL,
394 },
395 });
396
397
398module.exports = freeze({
399 TrustedHTML,
400 TrustedResourceURL,
401 TrustedScript,
402 TrustedURL,
403});