UNPKG

@angular/build

Version:

Official build system for Angular

294 lines (293 loc) 12.8 kB
"use strict"; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.hashTextContent = hashTextContent; exports.autoCsp = autoCsp; const crypto = __importStar(require("node:crypto")); const html_rewriting_stream_1 = require("./html-rewriting-stream"); /** * The hash function to use for hash directives to use in the CSP. */ const HASH_FUNCTION = 'sha256'; /** * Get the specified attribute or return undefined if the tag doesn't have that attribute. * * @param tag StartTag of the <script> * @returns */ function getScriptAttributeValue(tag, attrName) { return tag.attrs.find((attr) => attr.name === attrName)?.value; } /** * Checks whether a particular string is a MIME type associated with JavaScript, according to * https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types#textjavascript * * @param mimeType a string that may be a MIME type * @returns whether the string is a MIME type that is associated with JavaScript */ function isJavascriptMimeType(mimeType) { return mimeType.split(';')[0] === 'text/javascript'; } /** * Which of the type attributes on the script tag we should try passing along * based on https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type * @param scriptType the `type` attribute on the `<script>` tag under question * @returns whether to add the script tag to the dynamically loaded script tag */ function shouldDynamicallyLoadScriptTagBasedOnType(scriptType) { return (scriptType === undefined || scriptType === '' || scriptType === 'module' || isJavascriptMimeType(scriptType)); } /** * Calculates a CSP compatible hash of an inline script. * @param scriptText Text between opening and closing script tag. Has to * include whitespaces and newlines! * @returns The hash of the text formatted appropriately for CSP. */ function hashTextContent(scriptText) { const hash = crypto.createHash(HASH_FUNCTION).update(scriptText, 'utf-8').digest('base64'); return `'${HASH_FUNCTION}-${hash}'`; } /** * Finds all `<script>` tags and creates a dynamic script loading block for consecutive `<script>` with `src` attributes. * Hashes all scripts, both inline and generated dynamic script loading blocks. * Inserts a `<meta>` tag at the end of the `<head>` of the document with the generated hash-based CSP. * * @param html Markup that should be processed. * @returns The transformed HTML that contains the `<meta>` tag CSP and dynamic loader scripts. */ async function autoCsp(html, unsafeEval = false) { const { rewriter, transformedContent } = await (0, html_rewriting_stream_1.htmlRewritingStream)(html); let openedScriptTag = undefined; let scriptContent = []; const hashes = []; /** * Generates the dynamic loading script and puts it in the rewriter and adds the hash of the dynamic * loader script to the collection of hashes to add to the <meta> tag CSP. */ function emitLoaderScript() { const loaderScript = createLoaderScript(scriptContent, /* enableTrustedTypes = */ false); hashes.push(hashTextContent(loaderScript)); rewriter.emitRaw(`<script>${loaderScript}</script>`); scriptContent = []; } rewriter.on('startTag', (tag) => { if (tag.tagName === 'script') { openedScriptTag = tag; const src = getScriptAttributeValue(tag, 'src'); if (src) { // If there are any interesting attributes, note them down. const scriptType = getScriptAttributeValue(tag, 'type'); if (shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) { scriptContent.push({ src: src, type: scriptType, async: getScriptAttributeValue(tag, 'async') !== undefined, defer: getScriptAttributeValue(tag, 'defer') !== undefined, }); return; // Skip writing my script tag until we've read it all. } } } // We are encountering the first start tag that's not <script src="..."> after a string of // consecutive <script src="...">. <script> tags without a src attribute will also end a chain // of src attributes that can be loaded in a single loader script, so those will end up here. // // The first place when we can determine this to be the case is // during the first opening tag that's not <script src="...">, where we need to insert the // dynamic loader script before continuing on with writing the rest of the tags. // (One edge case is where there are no more opening tags after the last <script src="..."> is // closed, but this case is handled below with the final </body> tag.) if (scriptContent.length > 0) { emitLoaderScript(); } rewriter.emitStartTag(tag); }); rewriter.on('text', (tag, html) => { if (openedScriptTag && !getScriptAttributeValue(openedScriptTag, 'src')) { hashes.push(hashTextContent(html)); } rewriter.emitText(tag); }); rewriter.on('endTag', (tag, html) => { if (openedScriptTag && tag.tagName === 'script') { const src = getScriptAttributeValue(openedScriptTag, 'src'); const scriptType = getScriptAttributeValue(openedScriptTag, 'type'); openedScriptTag = undefined; // Return early to avoid writing the closing </script> tag if it's a part of the // dynamic loader script. if (src && shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) { return; } } if (tag.tagName === 'head' || tag.tagName === 'body' || tag.tagName === 'html') { // Write the loader script if a string of <script>s were the last opening tag of the document. if (scriptContent.length > 0) { emitLoaderScript(); } } rewriter.emitEndTag(tag); }); const rewritten = await transformedContent(); // Second pass to add the header const secondPass = await (0, html_rewriting_stream_1.htmlRewritingStream)(rewritten); secondPass.rewriter.on('startTag', (tag, _) => { secondPass.rewriter.emitStartTag(tag); if (tag.tagName === 'head') { // See what hashes we came up with! secondPass.rewriter.emitRaw(`<meta http-equiv="Content-Security-Policy" content="${getStrictCsp(hashes, { enableBrowserFallbacks: true, enableTrustedTypes: false, enableUnsafeEval: unsafeEval, })}">`); } }); return secondPass.transformedContent(); } /** * Returns a strict Content Security Policy for mitigating XSS. * For more details read csp.withgoogle.com. * If you modify this CSP, make sure it has not become trivially bypassable by * checking the policy using csp-evaluator.withgoogle.com. * * @param hashes A list of sha-256 hashes of trusted inline scripts. * @param enableTrustedTypes If Trusted Types should be enabled for scripts. * @param enableBrowserFallbacks If fallbacks for older browsers should be * added. This is will not weaken the policy as modern browsers will ignore * the fallbacks. * @param enableUnsafeEval If you cannot remove all uses of eval(), you can * still set a strict CSP, but you will have to use the 'unsafe-eval' * keyword which will make your policy slightly less secure. */ function getStrictCsp(hashes, // default CSP options cspOptions = { enableBrowserFallbacks: true, enableTrustedTypes: false, enableUnsafeEval: false, }) { hashes = hashes || []; const strictCspTemplate = { // 'strict-dynamic' allows hashed scripts to create new scripts. 'script-src': [`'strict-dynamic'`, ...hashes], // Restricts `object-src` to disable dangerous plugins like Flash. 'object-src': [`'none'`], // Restricts `base-uri` to block the injection of `<base>` tags. This // prevents attackers from changing the locations of scripts loaded from // relative URLs. 'base-uri': [`'self'`], }; // Adds fallbacks for browsers not compatible to CSP3 and CSP2. // These fallbacks are ignored by modern browsers in presence of hashes, // and 'strict-dynamic'. if (cspOptions.enableBrowserFallbacks) { // Fallback for Safari. All modern browsers supporting strict-dynamic will // ignore the 'https:' fallback. strictCspTemplate['script-src'].push('https:'); // 'unsafe-inline' is only ignored in presence of a hash or nonce. if (hashes.length > 0) { strictCspTemplate['script-src'].push(`'unsafe-inline'`); } } // If enabled, dangerous DOM sinks will only accept typed objects instead of // strings. if (cspOptions.enableTrustedTypes) { strictCspTemplate['require-trusted-types-for'] = ['script']; } // If enabled, `eval()`-calls will be allowed, making the policy slightly // less secure. if (cspOptions.enableUnsafeEval) { strictCspTemplate['script-src'].push(`'unsafe-eval'`); } return Object.entries(strictCspTemplate) .map(([directive, values]) => { return `${directive} ${values.join(' ')};`; }) .join(''); } /** * Returns JS code for dynamically loading sourced (external) scripts. * @param srcList A list of paths for scripts that should be loaded. */ function createLoaderScript(srcList, enableTrustedTypes = false) { if (!srcList.length) { throw new Error('Cannot create a loader script with no scripts to load.'); } const srcListFormatted = srcList .map((s) => { // URI encoding means value can't escape string, JS, or HTML context. const srcAttr = encodeURI(s.src).replaceAll("'", "\\'"); // Can only be 'module' or a JS MIME type or an empty string. const typeAttr = s.type ? "'" + s.type + "'" : "''"; const asyncAttr = s.async ? 'true' : 'false'; const deferAttr = s.defer ? 'true' : 'false'; return `['${srcAttr}', ${typeAttr}, ${asyncAttr}, ${deferAttr}]`; }) .join(); return enableTrustedTypes ? ` var scripts = [${srcListFormatted}]; var policy = self.trustedTypes && self.trustedTypes.createPolicy ? self.trustedTypes.createPolicy('angular#auto-csp', {createScriptURL: function(u) { return scripts.includes(u) ? u : null; }}) : { createScriptURL: function(u) { return u; } }; scripts.forEach(function(scriptUrl) { var s = document.createElement('script'); s.src = policy.createScriptURL(scriptUrl[0]); s.type = scriptUrl[1]; s.async = !!scriptUrl[2]; s.defer = !!scriptUrl[3]; document.lastElementChild.appendChild(s); });\n` : ` var scripts = [${srcListFormatted}]; scripts.forEach(function(scriptUrl) { var s = document.createElement('script'); s.src = scriptUrl[0]; s.type = scriptUrl[1]; s.async = !!scriptUrl[2]; s.defer = !!scriptUrl[3]; document.lastElementChild.appendChild(s); });\n`; }