From 5850fe93c5d23d27b1e13d3801db154568bfcf22 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 14 May 2025 14:39:48 +0100 Subject: [PATCH 1/8] feat(csp): pass external hashes --- packages/astro/src/core/build/generate.ts | 12 +++++-- .../src/core/build/plugins/plugin-manifest.ts | 12 +++++-- .../astro/src/core/config/schemas/base.ts | 2 ++ .../astro/src/core/config/schemas/refined.ts | 31 ++++++++++++++++ packages/astro/src/core/csp/common.ts | 30 ++++++++++++++++ packages/astro/src/types/public/config.ts | 30 +++++++++++++++- .../src/vite-plugin-astro-server/plugin.ts | 11 ++++-- packages/astro/test/csp.test.js | 34 ++++++++++++++++++ .../test/units/config/config-validate.test.js | 36 +++++++++++++++++++ 9 files changed, 190 insertions(+), 8 deletions(-) diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 48cc8efc07aa..23a7bb5fd013 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -30,6 +30,8 @@ import type { import type { SSRActions, SSRManifestCSP, SSRManifest, SSRManifestI18n } from '../app/types.js'; import { getAlgorithm, + getScriptHashes, + getStyleHashes, shouldTrackCspHashes, trackScriptHashes, trackStyleHashes, @@ -632,8 +634,14 @@ async function createBuildManifest( if (shouldTrackCspHashes(settings.config)) { const algorithm = getAlgorithm(settings.config); - const clientScriptHashes = await trackScriptHashes(internals, settings, algorithm); - const clientStyleHashes = await trackStyleHashes(internals, settings, algorithm); + const clientScriptHashes = [ + ...getScriptHashes(settings.config), + ...(await trackScriptHashes(internals, settings, algorithm)), + ]; + const clientStyleHashes = [ + ...getStyleHashes(settings.config), + ...(await trackStyleHashes(internals, settings, algorithm)), + ]; csp = { clientStyleHashes, diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 037a0e508e23..17c27bddfa66 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -14,6 +14,8 @@ import type { } from '../../app/types.js'; import { getAlgorithm, + getScriptHashes, + getStyleHashes, shouldTrackCspHashes, trackScriptHashes, trackStyleHashes, @@ -285,8 +287,14 @@ async function buildManifest( if (shouldTrackCspHashes(settings.config)) { const algorithm = getAlgorithm(settings.config); - const clientScriptHashes = await trackScriptHashes(internals, opts.settings, algorithm); - const clientStyleHashes = await trackStyleHashes(internals, opts.settings, algorithm); + const clientScriptHashes = [ + ...getScriptHashes(settings.config), + ...(await trackScriptHashes(internals, settings, algorithm)), + ]; + const clientStyleHashes = [ + ...getStyleHashes(settings.config), + ...(await trackStyleHashes(internals, settings, algorithm)), + ]; csp = { clientStyleHashes, diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts index 42a00a8bdf73..845e27b47f95 100644 --- a/packages/astro/src/core/config/schemas/base.ts +++ b/packages/astro/src/core/config/schemas/base.ts @@ -480,6 +480,8 @@ export const AstroConfigSchema = z.object({ z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.csp), z.object({ algorithm: cspAlgorithmSchema, + styleHashes: z.array(z.string()).optional(), + scriptHashes: z.array(z.string()).optional(), }), ]) .optional() diff --git a/packages/astro/src/core/config/schemas/refined.ts b/packages/astro/src/core/config/schemas/refined.ts index c14e18f12c92..2a756c93557f 100644 --- a/packages/astro/src/core/config/schemas/refined.ts +++ b/packages/astro/src/core/config/schemas/refined.ts @@ -203,4 +203,35 @@ export const AstroConfigRefinedSchema = z.custom().superRefine((con } } } + + if (config.experimental.csp && typeof config.experimental.csp === 'object') { + const { scriptHashes, styleHashes } = config.experimental.csp; + if (scriptHashes) { + for (const hash of scriptHashes) { + if ( + !(hash.startsWith('sha256-') || hash.startsWith('sha384-') || hash.startsWith('sha512-')) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `**scriptHashes** property "${hash}" must start with "sha256-", "sha384-" or "sha512-" to be valid.`, + path: ['experimental', 'csp', 'scriptHashes'], + }); + } + } + } + + if (styleHashes) { + for (const hash of styleHashes) { + if ( + !(hash.startsWith('sha256-') || hash.startsWith('sha384-') || hash.startsWith('sha512-')) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `**styleHashes** property "${hash}" must start with "sha256-", "sha384-" or "sha512-" to be valid.`, + path: ['experimental', 'csp', 'styleHashes'], + }); + } + } + } + } }); diff --git a/packages/astro/src/core/csp/common.ts b/packages/astro/src/core/csp/common.ts index 61dcb12fe247..93970bee66b9 100644 --- a/packages/astro/src/core/csp/common.ts +++ b/packages/astro/src/core/csp/common.ts @@ -28,6 +28,36 @@ export function getAlgorithm(config: AstroConfig): CspAlgorithm { return config.experimental.csp.algorithm; } +/** + * Use this function when after you checked that CSP is enabled, or it throws an error. + * @param config + */ +export function getScriptHashes(config: AstroConfig): string[] { + if (!config.experimental?.csp) { + throw new Error('CSP is not enabled'); + } + if (config.experimental?.csp === true) { + return []; + } else { + return config.experimental.csp.scriptHashes ?? []; + } +} + +/** + * Use this function when after you checked that CSP is enabled, or it throws an error. + * @param config + */ +export function getStyleHashes(config: AstroConfig): string[] { + if (!config.experimental?.csp) { + throw new Error('CSP is not enabled'); + } + if (config.experimental?.csp === true) { + return []; + } else { + return config.experimental.csp.styleHashes ?? []; + } +} + export async function trackStyleHashes( internals: BuildInternals, settings: AstroSettings, diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index f7eee823fb09..12dddef56124 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -2244,7 +2244,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { | { /** * @name experimental.csp.algorithm - * @type {string} + * @type {"SHA-256" | "SHA-384" | "SHA-512"} * @default `'SHA-256'` * @version 5.5.x * @description @@ -2255,6 +2255,34 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * */ algorithm?: CspAlgorithm; + + /** + * @name experimental.csp.styleHashes + * @type {string[]} + * @default `[]` + * @version 5.5.x + * @description + * + * A list of style hashes to include in all pages. These hashes are added to the `style-src` policy. + * + * The default value is `[]`. + * + */ + styleHashes?: string[]; + + /** + * @name experimental.csp.scriptHashes + * @type {string[]} + * @default `[]` + * @version 5.5.x + * @description + * + * A list of script hashes to include in all pages. These hashes are added to the `script-src` policy. + * + * The default value is `[]`. + * + */ + scriptHashes?: string[]; }; /** diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 7deb62d13d63..436407d589ad 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -5,7 +5,12 @@ import { fileURLToPath } from 'node:url'; import type * as vite from 'vite'; import { normalizePath } from 'vite'; import type { SSRManifestCSP, SSRManifest, SSRManifestI18n } from '../core/app/types.js'; -import { getAlgorithm, shouldTrackCspHashes } from '../core/csp/common.js'; +import { + getAlgorithm, + getScriptHashes, + getStyleHashes, + shouldTrackCspHashes, +} from '../core/csp/common.js'; import { warnMissingAdapter } from '../core/dev/adapter-validation.js'; import { createKey, getEnvironmentKey, hasEnvironmentKey } from '../core/encryption.js'; import { getViteErrorPayload } from '../core/errors/dev/index.js'; @@ -177,8 +182,8 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest if (shouldTrackCspHashes(settings.config)) { csp = { - clientScriptHashes: [], - clientStyleHashes: [], + clientScriptHashes: getScriptHashes(settings.config), + clientStyleHashes: getStyleHashes(settings.config), algorithm: getAlgorithm(settings.config), }; } diff --git a/packages/astro/test/csp.test.js b/packages/astro/test/csp.test.js index 3516b0f1f2ee..3456408bfcf2 100644 --- a/packages/astro/test/csp.test.js +++ b/packages/astro/test/csp.test.js @@ -102,4 +102,38 @@ describe('CSP', () => { assert.fail('Should have the manifest'); } }); + + it('should render hashes provided by the user', async () => { + fixture = await loadFixture({ + root: './fixtures/csp/', + adapter: testAdapter({ + setManifest(_manifest) { + manifest = _manifest; + }, + }), + experimental: { + csp: { + styleHashes: ['sha512-hash1', 'sha384-hash2'], + scriptHashes: ['sha512-hash3', 'sha384-hash4'], + }, + }, + }); + await fixture.build(); + app = await fixture.loadTestAdapterApp(); + + if (manifest) { + const request = new Request('http://example.com/index.html'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerio.load(html); + + const meta = $('meta[http-equiv="Content-Security-Policy"]'); + assert.ok(meta.attr('content').toString().includes('sha384-hash2')); + assert.ok(meta.attr('content').toString().includes('sha384-hash4')); + assert.ok(meta.attr('content').toString().includes('sha512-hash1')); + assert.ok(meta.attr('content').toString().includes('sha512-hash3')); + } else { + assert.fail('Should have the manifest'); + } + }); }); diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.js index 9c8f6d44b8ad..f6d98a92c552 100644 --- a/packages/astro/test/units/config/config-validate.test.js +++ b/packages/astro/test/units/config/config-validate.test.js @@ -479,4 +479,40 @@ describe('Config Validation', () => { ); }); }); + + describe('csp', () => { + it('should throw an error if incorrect scriptHashes are passed', async () => { + let configError = await validateConfig({ + experimental: { + csp: { + scriptHashes: ['fancy-1234567890'], + }, + }, + }).catch((err) => err); + assert.equal(configError instanceof z.ZodError, true); + assert.equal( + configError.errors[0].message.includes( + '**scriptHashes** property "fancy-1234567890" must start with "sha256-", "sha384-" or "sha512-" to be valid.', + ), + true, + ); + }); + + it('should throw an error if incorrect styleHashes are passed', async () => { + let configError = await validateConfig({ + experimental: { + csp: { + styleHashes: ['fancy-1234567890'], + }, + }, + }).catch((err) => err); + assert.equal(configError instanceof z.ZodError, true); + assert.equal( + configError.errors[0].message.includes( + '**styleHashes** property "fancy-1234567890" must start with "sha256-", "sha384-" or "sha512-" to be valid.', + ), + true, + ); + }); + }); }); From ee3fd241933ae497109ddb888038c5a460f2379b Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 15 May 2025 11:27:56 +0100 Subject: [PATCH 2/8] apply feedback --- packages/astro/src/core/build/generate.ts | 8 +-- .../src/core/build/plugins/plugin-manifest.ts | 8 +-- packages/astro/src/core/csp/common.ts | 49 ++++++------------- packages/astro/src/types/public/config.ts | 4 +- .../src/vite-plugin-astro-server/plugin.ts | 8 +-- 5 files changed, 29 insertions(+), 48 deletions(-) diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 23a7bb5fd013..e452494e4acc 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -632,14 +632,14 @@ async function createBuildManifest( }; } - if (shouldTrackCspHashes(settings.config)) { - const algorithm = getAlgorithm(settings.config); + if (shouldTrackCspHashes(settings.config.experimental.csp)) { + const algorithm = getAlgorithm(settings.config.experimental.csp); const clientScriptHashes = [ - ...getScriptHashes(settings.config), + ...getScriptHashes(settings.config.experimental.csp), ...(await trackScriptHashes(internals, settings, algorithm)), ]; const clientStyleHashes = [ - ...getStyleHashes(settings.config), + ...getStyleHashes(settings.config.experimental.csp), ...(await trackStyleHashes(internals, settings, algorithm)), ]; diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 17c27bddfa66..439b3f5ca59f 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -285,14 +285,14 @@ async function buildManifest( let csp = undefined; - if (shouldTrackCspHashes(settings.config)) { - const algorithm = getAlgorithm(settings.config); + if (shouldTrackCspHashes(settings.config.experimental.csp)) { + const algorithm = getAlgorithm(settings.config.experimental.csp); const clientScriptHashes = [ - ...getScriptHashes(settings.config), + ...getScriptHashes(settings.config.experimental.csp), ...(await trackScriptHashes(internals, settings, algorithm)), ]; const clientStyleHashes = [ - ...getStyleHashes(settings.config), + ...getStyleHashes(settings.config.experimental.csp), ...(await trackStyleHashes(internals, settings, algorithm)), ]; diff --git a/packages/astro/src/core/csp/common.ts b/packages/astro/src/core/csp/common.ts index 93970bee66b9..ba853c74e59b 100644 --- a/packages/astro/src/core/csp/common.ts +++ b/packages/astro/src/core/csp/common.ts @@ -8,53 +8,34 @@ import type { AstroConfig, CspAlgorithm } from '../../types/public/index.js'; import type { BuildInternals } from '../build/internal.js'; import { generateCspDigest } from '../encryption.js'; -export function shouldTrackCspHashes(config: AstroConfig): boolean { - return config.experimental?.csp === true || typeof config.experimental?.csp === 'object'; +export function shouldTrackCspHashes( + csp: any, +): csp is Exclude { + return csp === true || typeof csp === 'object'; } -/** - * Use this function when after you checked that CSP is enabled, or it throws an error. - * @param config - */ -export function getAlgorithm(config: AstroConfig): CspAlgorithm { - if (!config.experimental?.csp) { - // A regular error is fine here because this code should never be reached - // if CSP is not enabled - throw new Error('CSP is not enabled'); - } - if (config.experimental.csp === true) { +export function getAlgorithm( + csp: Exclude, +): CspAlgorithm { + if (csp === true) { return 'SHA-256'; } - return config.experimental.csp.algorithm; + return csp.algorithm; } -/** - * Use this function when after you checked that CSP is enabled, or it throws an error. - * @param config - */ -export function getScriptHashes(config: AstroConfig): string[] { - if (!config.experimental?.csp) { - throw new Error('CSP is not enabled'); - } - if (config.experimental?.csp === true) { +export function getScriptHashes(csp: Exclude): string[] { + if (csp === true) { return []; } else { - return config.experimental.csp.scriptHashes ?? []; + return csp.scriptHashes ?? []; } } -/** - * Use this function when after you checked that CSP is enabled, or it throws an error. - * @param config - */ -export function getStyleHashes(config: AstroConfig): string[] { - if (!config.experimental?.csp) { - throw new Error('CSP is not enabled'); - } - if (config.experimental?.csp === true) { +export function getStyleHashes(csp: Exclude): string[] { + if (csp === true) { return []; } else { - return config.experimental.csp.styleHashes ?? []; + return csp.styleHashes ?? []; } } diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 12dddef56124..e558aae8bb4f 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -2268,7 +2268,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * The default value is `[]`. * */ - styleHashes?: string[]; + styleHashes?: `${'sha256' | 'sha512' | 'sha384'}-${string}`[]; /** * @name experimental.csp.scriptHashes @@ -2282,7 +2282,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * The default value is `[]`. * */ - scriptHashes?: string[]; + scriptHashes?: `${'sha256' | 'sha512' | 'sha384'}-${string}`[]; }; /** diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 436407d589ad..96da8f423c56 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -180,11 +180,11 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest }; } - if (shouldTrackCspHashes(settings.config)) { + if (shouldTrackCspHashes(settings.config.experimental.csp)) { csp = { - clientScriptHashes: getScriptHashes(settings.config), - clientStyleHashes: getStyleHashes(settings.config), - algorithm: getAlgorithm(settings.config), + clientScriptHashes: getScriptHashes(settings.config.experimental.csp), + clientStyleHashes: getStyleHashes(settings.config.experimental.csp), + algorithm: getAlgorithm(settings.config.experimental.csp), }; } From 53f327ae677a6f80593e557183779ebb67862cbe Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 15 May 2025 13:18:09 +0100 Subject: [PATCH 3/8] apply feedback --- .../astro/src/core/config/schemas/refined.ts | 33 ++++++++++--------- packages/astro/src/core/csp/common.ts | 14 ++++---- packages/astro/src/core/csp/config.ts | 10 +++++- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/packages/astro/src/core/config/schemas/refined.ts b/packages/astro/src/core/config/schemas/refined.ts index 2a756c93557f..5f7111b3a23f 100644 --- a/packages/astro/src/core/config/schemas/refined.ts +++ b/packages/astro/src/core/config/schemas/refined.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import type { AstroConfig } from '../../../types/public/config.js'; +import { ALGORITHM_VALUES } from '../../csp/config.js'; export const AstroConfigRefinedSchema = z.custom().superRefine((config, ctx) => { if ( @@ -208,28 +209,28 @@ export const AstroConfigRefinedSchema = z.custom().superRefine((con const { scriptHashes, styleHashes } = config.experimental.csp; if (scriptHashes) { for (const hash of scriptHashes) { - if ( - !(hash.startsWith('sha256-') || hash.startsWith('sha384-') || hash.startsWith('sha512-')) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `**scriptHashes** property "${hash}" must start with "sha256-", "sha384-" or "sha512-" to be valid.`, - path: ['experimental', 'csp', 'scriptHashes'], - }); + for (const allowedValue of ALGORITHM_VALUES) { + if (!hash.startsWith(allowedValue)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `**scriptHashes** property "${hash}" must start with with one of following values: ${ALGORITHM_VALUES.join(', ')}`, + path: ['experimental', 'csp', 'scriptHashes'], + }); + } } } } if (styleHashes) { for (const hash of styleHashes) { - if ( - !(hash.startsWith('sha256-') || hash.startsWith('sha384-') || hash.startsWith('sha512-')) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `**styleHashes** property "${hash}" must start with "sha256-", "sha384-" or "sha512-" to be valid.`, - path: ['experimental', 'csp', 'styleHashes'], - }); + for (const allowedValue of ALGORITHM_VALUES) { + if (!hash.startsWith(allowedValue)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `**styleHashes** property "${hash}" must start with with one of following values: ${ALGORITHM_VALUES.join(', ')}`, + path: ['experimental', 'csp', 'styleHashes'], + }); + } } } } diff --git a/packages/astro/src/core/csp/common.ts b/packages/astro/src/core/csp/common.ts index ba853c74e59b..b3e56f9bab11 100644 --- a/packages/astro/src/core/csp/common.ts +++ b/packages/astro/src/core/csp/common.ts @@ -8,22 +8,20 @@ import type { AstroConfig, CspAlgorithm } from '../../types/public/index.js'; import type { BuildInternals } from '../build/internal.js'; import { generateCspDigest } from '../encryption.js'; -export function shouldTrackCspHashes( - csp: any, -): csp is Exclude { +type EnabledCsp = Exclude; + +export function shouldTrackCspHashes(csp: any): csp is EnabledCsp { return csp === true || typeof csp === 'object'; } -export function getAlgorithm( - csp: Exclude, -): CspAlgorithm { +export function getAlgorithm(csp: EnabledCsp): CspAlgorithm { if (csp === true) { return 'SHA-256'; } return csp.algorithm; } -export function getScriptHashes(csp: Exclude): string[] { +export function getScriptHashes(csp: EnabledCsp): string[] { if (csp === true) { return []; } else { @@ -31,7 +29,7 @@ export function getScriptHashes(csp: Exclude): string[] { +export function getStyleHashes(csp: EnabledCsp): string[] { if (csp === true) { return []; } else { diff --git a/packages/astro/src/core/csp/config.ts b/packages/astro/src/core/csp/config.ts index f3c61878326a..61515df25372 100644 --- a/packages/astro/src/core/csp/config.ts +++ b/packages/astro/src/core/csp/config.ts @@ -1,8 +1,16 @@ import { z } from 'zod'; +export const ALGORITHMS = { + 'SHA-256': 'sha256', + 'SHA-384': 'sha384', + 'SHA-512': 'sha512', +} as const; + +export const ALGORITHM_VALUES = Object.values(ALGORITHMS); + export const cspAlgorithmSchema = z .enum(['SHA-512', 'SHA-384', 'SHA-256']) .optional() .default('SHA-256'); -export type CspAlgorithm = z.infer; +export type CspAlgorithm = keyof typeof ALGORITHMS; From 3d8c8b31878f07ca22587bb10ddd1315cd43b1f5 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 15 May 2025 13:50:23 +0100 Subject: [PATCH 4/8] fix test --- package.json | 2 +- packages/astro/src/core/config/schemas/refined.ts | 5 +++-- packages/astro/src/core/csp/config.ts | 8 ++++---- packages/astro/src/types/public/config.ts | 4 ++-- packages/astro/test/units/config/config-validate.test.js | 5 +++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 145078de4e71..d79a0e38fa2e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "test:e2e:match": "cd packages/astro && pnpm playwright install chromium firefox && pnpm run test:e2e:match", "test:e2e:hosts": "turbo run test:hosted", "benchmark": "astro-benchmark", - "lint": "biome lint && eslint . --report-unused-disable-directives-severity=warn && knip", + "lint": "biome lint && knip && eslint . --report-unused-disable-directives-severity=warn", "lint:ci": "biome ci --formatter-enabled=false --organize-imports-enabled=false --reporter=github && eslint . --report-unused-disable-directives-severity=warn && knip", "lint:fix": "biome lint --write --unsafe", "publint": "pnpm -r --filter=astro --filter=create-astro --filter=\"@astrojs/*\" --no-bail exec publint", diff --git a/packages/astro/src/core/config/schemas/refined.ts b/packages/astro/src/core/config/schemas/refined.ts index 5f7111b3a23f..3bb4af31ac33 100644 --- a/packages/astro/src/core/config/schemas/refined.ts +++ b/packages/astro/src/core/config/schemas/refined.ts @@ -210,10 +210,11 @@ export const AstroConfigRefinedSchema = z.custom().superRefine((con if (scriptHashes) { for (const hash of scriptHashes) { for (const allowedValue of ALGORITHM_VALUES) { + console.log(hash, allowedValue); if (!hash.startsWith(allowedValue)) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `**scriptHashes** property "${hash}" must start with with one of following values: ${ALGORITHM_VALUES.join(', ')}`, + message: `**scriptHashes** property "${hash}" must start with with one of following values: ${ALGORITHM_VALUES.join(', ')}.`, path: ['experimental', 'csp', 'scriptHashes'], }); } @@ -227,7 +228,7 @@ export const AstroConfigRefinedSchema = z.custom().superRefine((con if (!hash.startsWith(allowedValue)) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `**styleHashes** property "${hash}" must start with with one of following values: ${ALGORITHM_VALUES.join(', ')}`, + message: `**styleHashes** property "${hash}" must start with with one of following values: ${ALGORITHM_VALUES.join(', ')}.`, path: ['experimental', 'csp', 'styleHashes'], }); } diff --git a/packages/astro/src/core/csp/config.ts b/packages/astro/src/core/csp/config.ts index 61515df25372..c1cb425f5889 100644 --- a/packages/astro/src/core/csp/config.ts +++ b/packages/astro/src/core/csp/config.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -export const ALGORITHMS = { - 'SHA-256': 'sha256', - 'SHA-384': 'sha384', - 'SHA-512': 'sha512', +const ALGORITHMS = { + 'SHA-256': 'sha256-', + 'SHA-384': 'sha384-', + 'SHA-512': 'sha512-', } as const; export const ALGORITHM_VALUES = Object.values(ALGORITHMS); diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index e558aae8bb4f..f8ec143d7253 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -2268,7 +2268,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * The default value is `[]`. * */ - styleHashes?: `${'sha256' | 'sha512' | 'sha384'}-${string}`[]; + styleHashes?: `${CspAlgorithm}-${string}`[]; /** * @name experimental.csp.scriptHashes @@ -2282,7 +2282,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * The default value is `[]`. * */ - scriptHashes?: `${'sha256' | 'sha512' | 'sha384'}-${string}`[]; + scriptHashes?: `${CspAlgorithm}-${string}`[]; }; /** diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.js index f6d98a92c552..3d0fc1cee5d4 100644 --- a/packages/astro/test/units/config/config-validate.test.js +++ b/packages/astro/test/units/config/config-validate.test.js @@ -490,9 +490,10 @@ describe('Config Validation', () => { }, }).catch((err) => err); assert.equal(configError instanceof z.ZodError, true); + console.log(configError.errors[0].message); assert.equal( configError.errors[0].message.includes( - '**scriptHashes** property "fancy-1234567890" must start with "sha256-", "sha384-" or "sha512-" to be valid.', + '**scriptHashes** property "fancy-1234567890" must start with with one of following values: sha256-, sha384-, sha512-.', ), true, ); @@ -509,7 +510,7 @@ describe('Config Validation', () => { assert.equal(configError instanceof z.ZodError, true); assert.equal( configError.errors[0].message.includes( - '**styleHashes** property "fancy-1234567890" must start with "sha256-", "sha384-" or "sha512-" to be valid.', + '**styleHashes** property "fancy-1234567890" must start with with one of following values: sha256-, sha384-, sha512-.', ), true, ); From 6eba6ab116b5eef25c91e3c456423e96645dde98 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 15 May 2025 13:52:44 +0100 Subject: [PATCH 5/8] ops --- packages/astro/src/core/config/schemas/refined.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/astro/src/core/config/schemas/refined.ts b/packages/astro/src/core/config/schemas/refined.ts index 3bb4af31ac33..61d9e93689dd 100644 --- a/packages/astro/src/core/config/schemas/refined.ts +++ b/packages/astro/src/core/config/schemas/refined.ts @@ -210,7 +210,6 @@ export const AstroConfigRefinedSchema = z.custom().superRefine((con if (scriptHashes) { for (const hash of scriptHashes) { for (const allowedValue of ALGORITHM_VALUES) { - console.log(hash, allowedValue); if (!hash.startsWith(allowedValue)) { ctx.addIssue({ code: z.ZodIssueCode.custom, From ab20b18215ac59d53b563342cb4b1718540f3deb Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 15 May 2025 15:23:36 +0200 Subject: [PATCH 6/8] fix: types --- packages/astro/src/core/csp/config.ts | 23 +++++++++++++++++++---- packages/astro/src/types/public/config.ts | 6 +++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/astro/src/core/csp/config.ts b/packages/astro/src/core/csp/config.ts index c1cb425f5889..249f54796d2f 100644 --- a/packages/astro/src/core/csp/config.ts +++ b/packages/astro/src/core/csp/config.ts @@ -1,16 +1,31 @@ import { z } from 'zod'; +type UnionToIntersection = (U extends never ? never : (arg: U) => never) extends ( + arg: infer I, +) => void + ? I + : never; + +type UnionToTuple = UnionToIntersection T> extends ( + _: never, +) => infer W + ? [...UnionToTuple>, W] + : []; + const ALGORITHMS = { 'SHA-256': 'sha256-', 'SHA-384': 'sha384-', 'SHA-512': 'sha512-', } as const; -export const ALGORITHM_VALUES = Object.values(ALGORITHMS); +type Algorithms = typeof ALGORITHMS; + +export type CspAlgorithm = keyof Algorithms; +export type CspAlgorithmValue = Algorithms[keyof Algorithms]; + +export const ALGORITHM_VALUES = Object.values(ALGORITHMS) as UnionToTuple; export const cspAlgorithmSchema = z - .enum(['SHA-512', 'SHA-384', 'SHA-256']) + .enum(Object.keys(ALGORITHMS) as UnionToTuple) .optional() .default('SHA-256'); - -export type CspAlgorithm = keyof typeof ALGORITHMS; diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index f8ec143d7253..9f7ef6c4f74b 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -18,7 +18,7 @@ import type { AstroCookieSetOptions } from '../../core/cookies/cookies.js'; import type { Logger, LoggerLevel } from '../../core/logger/core.js'; import type { EnvSchema } from '../../env/schema.js'; import type { AstroIntegration } from './integrations.js'; -import type { CspAlgorithm } from '../../core/csp/config.js'; +import type { CspAlgorithm, CspAlgorithmValue } from '../../core/csp/config.js'; export type Locales = (string | { codes: [string, ...string[]]; path: string })[]; @@ -2268,7 +2268,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * The default value is `[]`. * */ - styleHashes?: `${CspAlgorithm}-${string}`[]; + styleHashes?: `${CspAlgorithmValue}${string}`[]; /** * @name experimental.csp.scriptHashes @@ -2282,7 +2282,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * The default value is `[]`. * */ - scriptHashes?: `${CspAlgorithm}-${string}`[]; + scriptHashes?: `${CspAlgorithmValue}${string}`[]; }; /** From b37eb7b77374206d889406f65c56172d46afa1b3 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 15 May 2025 15:27:59 +0200 Subject: [PATCH 7/8] feat: type tests --- packages/astro/test/types/define-config.ts | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/astro/test/types/define-config.ts b/packages/astro/test/types/define-config.ts index 504d2b0bcb46..d31f5bfb7900 100644 --- a/packages/astro/test/types/define-config.ts +++ b/packages/astro/test/types/define-config.ts @@ -174,4 +174,27 @@ describe('defineConfig()', () => { }, ); }); + + it('Validates CSP hashes', () => { + defineConfig({ + experimental: { + csp: { + scriptHashes: [ + 'sha256-xx', + 'sha384-xx', + 'sha512-xx', + // @ts-expect-error doesn't have the correct prefix + 'fancy-1234567890', + ], + styleHashes: [ + 'sha256-xx', + 'sha384-xx', + 'sha512-xx', + // @ts-expect-error doesn't have the correct prefix + 'fancy-1234567890', + ], + }, + }, + }); + }); }); From ad5fa1eff6e28895f3fbfb69f04589d024cd9fde Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 15 May 2025 14:35:45 +0100 Subject: [PATCH 8/8] remove log --- packages/astro/test/units/config/config-validate.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.js index 3d0fc1cee5d4..6b22d53d5152 100644 --- a/packages/astro/test/units/config/config-validate.test.js +++ b/packages/astro/test/units/config/config-validate.test.js @@ -490,7 +490,6 @@ describe('Config Validation', () => { }, }).catch((err) => err); assert.equal(configError instanceof z.ZodError, true); - console.log(configError.errors[0].message); assert.equal( configError.errors[0].message.includes( '**scriptHashes** property "fancy-1234567890" must start with with one of following values: sha256-, sha384-, sha512-.',