Skip to content

Commit efb4341

Browse files
aaronshimdgp1130
authored andcommitted
feat(@angular/build): Auto-CSP support as a part of angular.json schema
Following up on the logic provided in #28639, we want to offer an opt-in option in angular.json to enable the auto-CSP transformation. For now, builds for `ng serve` will have Auto-CSP disabled.
1 parent b893a6a commit efb4341

File tree

7 files changed

+69
-2
lines changed

7 files changed

+69
-2
lines changed

goldens/public-api/angular/build/index.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface ApplicationBuilderOptions {
4848
preserveSymlinks?: boolean;
4949
progress?: boolean;
5050
scripts?: ScriptElement[];
51+
security?: Security;
5152
server?: string;
5253
serviceWorker?: ServiceWorker_2;
5354
sourceMap?: SourceMapUnion;

packages/angular/build/src/builders/application/execute-build.ts

+5
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ export async function executeBuild(
192192
);
193193
}
194194

195+
// Override auto-CSP settings if we are serving through Vite middleware.
196+
if (context.builder.builderName === 'dev-server' && options.security) {
197+
options.security.autoCsp = false;
198+
}
199+
195200
// Perform i18n translation inlining if enabled
196201
if (i18nOptions.shouldInline) {
197202
const result = await inlineI18n(options, executionResult, initialFiles);

packages/angular/build/src/builders/application/options.ts

+2
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ export async function normalizeOptions(
398398
partialSSRBuild = false,
399399
externalRuntimeStyles,
400400
instrumentForCoverage,
401+
security,
401402
} = options;
402403

403404
// Return all the normalized options
@@ -461,6 +462,7 @@ export async function normalizeOptions(
461462
partialSSRBuild: usePartialSsrBuild || partialSSRBuild,
462463
externalRuntimeStyles,
463464
instrumentForCoverage,
465+
security,
464466
};
465467
}
466468

packages/angular/build/src/builders/application/schema.json

+27
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,33 @@
3737
"type": "string",
3838
"description": "Customize the base path for the URLs of resources in 'index.html' and component stylesheets. This option is only necessary for specific deployment scenarios, such as with Angular Elements or when utilizing different CDN locations."
3939
},
40+
"security": {
41+
"description": "Security features to protect against XSS and other common attacks",
42+
"type": "object",
43+
"additionalProperties": false,
44+
"properties": {
45+
"autoCsp": {
46+
"description": "Enables automatic generation of a hash-based Strict Content Security Policy (https://web.dev/articles/strict-csp#choose-hash) based on scripts in index.html. Will default to true once we are out of experimental/preview phases.",
47+
"default": false,
48+
"oneOf": [
49+
{
50+
"type": "object",
51+
"properties": {
52+
"unsafeEval": {
53+
"type": "boolean",
54+
"description": "Include the `unsafe-eval` directive (https://web.dev/articles/strict-csp#remove-eval) in the auto-CSP. Please only enable this if you are absolutely sure that you need to, as allowing calls to eval will weaken the XSS defenses provided by the auto-CSP.",
55+
"default": false
56+
}
57+
},
58+
"additionalProperties": false
59+
},
60+
{
61+
"type": "boolean"
62+
}
63+
]
64+
}
65+
}
66+
},
4067
"scripts": {
4168
"description": "Global scripts to be included in the build.",
4269
"type": "array",

packages/angular/build/src/tools/esbuild/index-html-generator.ts

+10
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ export async function generateIndexHtml(
8080
throw new Error(`Output file does not exist: ${relativefilePath}`);
8181
};
8282

83+
// Read the Auto CSP options.
84+
const autoCsp = buildOptions.security?.autoCsp;
85+
const autoCspOptions =
86+
autoCsp === true
87+
? { unsafeEval: false }
88+
: autoCsp
89+
? { unsafeEval: !!autoCsp.unsafeEval }
90+
: undefined;
91+
8392
// Create an index HTML generator that reads from the in-memory output files
8493
const indexHtmlGenerator = new IndexHtmlGenerator({
8594
indexPath: indexHtmlOptions.input,
@@ -94,6 +103,7 @@ export async function generateIndexHtml(
94103
buildOptions.prerenderOptions ||
95104
buildOptions.appShellOptions
96105
),
106+
autoCsp: autoCspOptions,
97107
});
98108

99109
indexHtmlGenerator.readAsset = readAsset;

packages/angular/build/src/utils/index-file/auto-csp.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function hashTextContent(scriptText: string): string {
8080
* @param html Markup that should be processed.
8181
* @returns The transformed HTML that contains the `<meta>` tag CSP and dynamic loader scripts.
8282
*/
83-
export async function autoCsp(html: string): Promise<string> {
83+
export async function autoCsp(html: string, unsafeEval = false): Promise<string> {
8484
const { rewriter, transformedContent } = await htmlRewritingStream(html);
8585

8686
let openedScriptTag: StartTag | undefined = undefined;
@@ -170,7 +170,11 @@ export async function autoCsp(html: string): Promise<string> {
170170
if (tag.tagName === 'head') {
171171
// See what hashes we came up with!
172172
secondPass.rewriter.emitRaw(
173-
`<meta http-equiv="Content-Security-Policy" content="${getStrictCsp(hashes)}">`,
173+
`<meta http-equiv="Content-Security-Policy" content="${getStrictCsp(hashes, {
174+
enableBrowserFallbacks: true,
175+
enableTrustedTypes: false,
176+
enableUnsafeEval: unsafeEval,
177+
})}">`,
174178
);
175179
}
176180
});

packages/angular/build/src/utils/index-file/index-html-generator.ts

+18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { NormalizedCachedOptions } from '../normalize-cache';
1212
import { NormalizedOptimizationOptions } from '../normalize-optimization';
1313
import { addEventDispatchContract } from './add-event-dispatch-contract';
1414
import { CrossOriginValue, Entrypoint, FileInfo, augmentIndexHtml } from './augment-index-html';
15+
import { autoCsp } from './auto-csp';
1516
import { InlineCriticalCssProcessor } from './inline-critical-css';
1617
import { InlineFontsProcessor } from './inline-fonts';
1718
import { addNgcmAttribute } from './ngcm-attribute';
@@ -32,6 +33,10 @@ export interface IndexHtmlGeneratorProcessOptions {
3233
hints?: { url: string; mode: HintMode; as?: string }[];
3334
}
3435

36+
export interface AutoCspOptions {
37+
unsafeEval: boolean;
38+
}
39+
3540
export interface IndexHtmlGeneratorOptions {
3641
indexPath: string;
3742
deployUrl?: string;
@@ -43,6 +48,7 @@ export interface IndexHtmlGeneratorOptions {
4348
cache?: NormalizedCachedOptions;
4449
imageDomains?: string[];
4550
generateDedicatedSSRContent?: boolean;
51+
autoCsp?: AutoCspOptions;
4652
}
4753

4854
export type IndexHtmlTransform = (content: string) => Promise<string>;
@@ -86,6 +92,14 @@ export class IndexHtmlGenerator {
8692
this.csrPlugins.push(addNgcmAttributePlugin());
8793
this.ssrPlugins.push(addEventDispatchContractPlugin(), addNoncePlugin());
8894
}
95+
96+
// Auto-CSP (as the last step)
97+
if (options.autoCsp) {
98+
if (options.generateDedicatedSSRContent) {
99+
throw new Error('Cannot set both SSR and auto-CSP at the same time.');
100+
}
101+
this.csrPlugins.push(autoCspPlugin(options.autoCsp.unsafeEval));
102+
}
89103
}
90104

91105
async process(options: IndexHtmlGeneratorProcessOptions): Promise<IndexHtmlProcessResult> {
@@ -198,6 +212,10 @@ function addNoncePlugin(): IndexHtmlGeneratorPlugin {
198212
return (html) => addNonce(html);
199213
}
200214

215+
function autoCspPlugin(unsafeEval: boolean): IndexHtmlGeneratorPlugin {
216+
return (html) => autoCsp(html, unsafeEval);
217+
}
218+
201219
function postTransformPlugin({ options }: IndexHtmlGenerator): IndexHtmlGeneratorPlugin {
202220
return async (html) => (options.postTransform ? options.postTransform(html) : html);
203221
}

0 commit comments

Comments
 (0)