Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import { realpathSync } from 'node:fs';
import { access, constants } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } from '../../utils';
import {
normalizeAssetPatterns,
normalizeHashFuncNames,
normalizeOptimization,
normalizeSourceMaps,
} from '../../utils';
import { supportColor } from '../../utils/color';
import { useJSONBuildLogs, usePartialSsrBuild } from '../../utils/environment-options';
import { I18nOptions, createI18nOptions } from '../../utils/i18n-options';
Expand Down Expand Up @@ -182,6 +187,7 @@ export async function normalizeOptions(
const assets = options.assets?.length
? normalizeAssetPatterns(options.assets, workspaceRoot, projectRoot, projectSourceRoot)
: undefined;
const hashFuncNames = normalizeHashFuncNames(options.hashFuncNames);

let fileReplacements: Record<string, string> | undefined;
if (options.fileReplacements) {
Expand Down Expand Up @@ -429,6 +435,7 @@ export async function normalizeOptions(
preserveSymlinks,
stylePreprocessorOptions,
subresourceIntegrity,
hashFuncNames,
serverEntryPoint,
prerenderOptions,
appShellOptions,
Expand Down
8 changes: 8 additions & 0 deletions packages/angular/build/src/builders/application/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,14 @@
"description": "Enables the use of subresource integrity validation.",
"default": false
},
"hashFuncNames": {
"description": "Overrides the default hash function names for subresource integrity validation.",
"type": "array",
"items": {
"type": "string"
},
"default": ["sha384"]
},
"serviceWorker": {
"description": "Generates a service worker configuration.",
"default": false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* @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
*/

import { logging } from '@angular-devkit/core';
import { buildApplication } from '../../index';
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';

describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
describe('Option: "hashFuncNames"', () => {
it(`does not add integrity attribute when not present`, async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
subresourceIntegrity: true,
});

const { result } = await harness.executeOnce();

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/index.html').content.not.toContain('integrity=');
});

it(`uses default sri algo when not supplied`, async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
});

const { result } = await harness.executeOnce();

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/index.html').content.toContain('integrity=sha384');
});

it(`will override hash algorithm if provided`, async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
hashFuncNames: ['sha256'],
});

const { result } = await harness.executeOnce();

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/index.html').content.toContain('integrity=sha256');
});

it(`will use the most secure hash algorithm if multiple are provided`, async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
hashFuncNames: ['sha256', 'sha512'],
});

const { result } = await harness.executeOnce();

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/index.html').content.toContain('integrity=sha512');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export async function generateIndexHtml(
optimizationOptions,
crossOrigin,
subresourceIntegrity,
hashFuncNames,
baseHref,
} = buildOptions;

Expand Down Expand Up @@ -94,6 +95,7 @@ export async function generateIndexHtml(
indexPath: indexHtmlOptions.input,
entrypoints: indexHtmlOptions.insertionOrder,
sri: subresourceIntegrity,
sriHashAlgo: hashFuncNames,
optimization: optimizationOptions,
crossOrigin: crossOrigin,
deployUrl: buildOptions.publicPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { extname } from 'node:path';
import { loadEsmModule } from '../load-esm';
import { htmlRewritingStream } from './html-rewriting-stream';
import { VALID_SELF_CLOSING_TAGS } from './valid-self-closing-tags';
import { AllowedSRIHash } from '../normalize-hash-func-names';

export type LoadOutputFileFunctionType = (file: string) => Promise<string>;

Expand All @@ -24,6 +25,7 @@ export interface AugmentIndexHtmlOptions {
baseHref?: string;
deployUrl?: string;
sri: boolean;
sriHashAlgo?: AllowedSRIHash;

/** crossorigin attribute setting of elements that provide CORS support */
crossOrigin?: CrossOriginValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { InlineCriticalCssProcessor } from './inline-critical-css';
import { InlineFontsProcessor } from './inline-fonts';
import { addNgcmAttribute } from './ngcm-attribute';
import { addNonce } from './nonce';
import { AllowedSRIHash } from '../normalize-hash-func-names';

type IndexHtmlGeneratorPlugin = (
html: string,
Expand All @@ -41,6 +42,7 @@ export interface IndexHtmlGeneratorOptions {
indexPath: string;
deployUrl?: string;
sri?: boolean;
sriHashAlgo?: AllowedSRIHash;
entrypoints: Entrypoint[];
postTransform?: IndexHtmlTransform;
crossOrigin?: CrossOriginValue;
Expand Down Expand Up @@ -168,7 +170,14 @@ export class IndexHtmlGenerator {
}

function augmentIndexHtmlPlugin(generator: IndexHtmlGenerator): IndexHtmlGeneratorPlugin {
const { deployUrl, crossOrigin, sri = false, entrypoints, imageDomains } = generator.options;
const {
deployUrl,
crossOrigin,
sri = false,
sriHashAlgo = 'sha384',
entrypoints,
imageDomains,
} = generator.options;

return async (html, options) => {
const { lang, baseHref, outputPath = '', files, hints } = options;
Expand All @@ -179,6 +188,7 @@ function augmentIndexHtmlPlugin(generator: IndexHtmlGenerator): IndexHtmlGenerat
deployUrl,
crossOrigin,
sri,
sriHashAlgo,
lang,
entrypoints,
loadOutputFile: (filePath) => generator.readAsset(join(outputPath, filePath)),
Expand Down
1 change: 1 addition & 0 deletions packages/angular/build/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

export * from './normalize-asset-patterns';
export * from './normalize-hash-func-names';
export * from './normalize-optimization';
export * from './normalize-source-maps';
export * from './load-proxy-config';
41 changes: 41 additions & 0 deletions packages/angular/build/src/utils/normalize-hash-func-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @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
*/

/**
* @description checks parameter against hash names according to current OpenSSL
* and W3C acceptable integrity hashes
* @param hashName hash to check
* @returns true for valid hash names
*/
const hashIsValid = (hashName: string) => ['sha256', 'sha384', 'sha512'].includes(hashName);

export type AllowedSRIHash = 'sha256' | 'sha384' | 'sha512';

// Considered best practice as of this writing
export const DEFAULT_SRI_HASH: AllowedSRIHash = 'sha384';

/**
* @description returns the strongest hash name from the given list
* @param hashes list of string hash names
* @returns string hash algorithm name
*/
export const getStrongestHash = (hashes: string[]): AllowedSRIHash => {
const filteredHashes = hashes.filter(hashIsValid) as AllowedSRIHash[];
const sortedHashes = [...filteredHashes].sort();
const strongestHashName = sortedHashes.pop() ?? DEFAULT_SRI_HASH;

return strongestHashName;
};

export const normalizeHashFuncNames = (hashFuncs?: string[]): AllowedSRIHash => {
if (!hashFuncs || hashFuncs.length === 0) {
return DEFAULT_SRI_HASH;
}

return getStrongestHash(hashFuncs);
};
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
if (subresourceIntegrity) {
extraPlugins.push(
new SubresourceIntegrityPlugin({
hashFuncNames: ['sha384'],
hashFuncNames: buildOptions.hashFuncNames ?? ['sha384'],
}),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface BuildOptions {
namedChunks?: boolean;
crossOrigin?: CrossOrigin;
subresourceIntegrity?: boolean;
hashFuncNames?: [string, ...string[]];
serviceWorker?: boolean;
webWorkerTsConfig?: string;
statsJson: boolean;
Expand Down