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