diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts
index dddd6b09e09e..df400ae85ec6 100644
--- a/packages/astro/src/container/index.ts
+++ b/packages/astro/src/container/index.ts
@@ -161,9 +161,7 @@ function createManifest(
checkOrigin: false,
middleware: manifest?.middleware ?? middlewareInstance,
key: createKey(),
- clientScriptHashes: manifest?.clientScriptHashes ?? [],
- clientStyleHashes: manifest?.clientStyleHashes ?? [],
- shouldInjectCspMetaTags: manifest?.shouldInjectCspMetaTags ?? false,
+ csp: manifest?.csp,
};
}
@@ -249,9 +247,7 @@ type AstroContainerManifest = Pick<
| 'publicDir'
| 'outDir'
| 'cacheDir'
- | 'clientScriptHashes'
- | 'clientStyleHashes'
- | 'shouldInjectCspMetaTags'
+ | 'csp'
>;
type AstroContainerConstructor = {
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
index 18f926eb37f0..4acfff63effa 100644
--- a/packages/astro/src/core/app/types.ts
+++ b/packages/astro/src/core/app/types.ts
@@ -3,7 +3,12 @@ import type { ActionAccept, ActionClient } from '../../actions/runtime/virtual/s
import type { RoutingStrategies } from '../../i18n/utils.js';
import type { ComponentInstance, SerializedRouteData } from '../../types/astro.js';
import type { AstroMiddlewareInstance } from '../../types/public/common.js';
-import type { AstroConfig, Locales, ResolvedSessionConfig } from '../../types/public/config.js';
+import type {
+ AstroConfig,
+ CspAlgorithm,
+ Locales,
+ ResolvedSessionConfig,
+} from '../../types/public/config.js';
import type {
RouteData,
SSRComponentMetadata,
@@ -86,12 +91,7 @@ export type SSRManifest = {
publicDir: string | URL;
buildClientDir: string | URL;
buildServerDir: string | URL;
- clientScriptHashes: string[];
- clientStyleHashes: string[];
- /**
- * When enabled, Astro tracks the hashes of script and styles, and eventually it will render the `` tag
- */
- shouldInjectCspMetaTags: boolean;
+ csp: SSRManifestCSP | undefined;
};
export type SSRActions = {
@@ -107,6 +107,12 @@ export type SSRManifestI18n = {
domainLookupTable: Record;
};
+export type SSRManifestCSP = {
+ algorithm: CspAlgorithm;
+ clientScriptHashes: string[];
+ clientStyleHashes: string[];
+};
+
/** Public type exposed through the `astro:build:ssr` integration hook */
export type SerializedSSRManifest = Omit<
SSRManifest,
diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts
index d239a4a2fae2..3ac79219588e 100644
--- a/packages/astro/src/core/base-pipeline.ts
+++ b/packages/astro/src/core/base-pipeline.ts
@@ -13,7 +13,6 @@ import type {
} from '../types/public/internal.js';
import { createOriginCheckMiddleware } from './app/middlewares.js';
import type { SSRActions } from './app/types.js';
-import { createCSPMiddleware } from './csp/middleware.js';
import { ActionNotFoundError } from './errors/errors-data.js';
import { AstroError } from './errors/index.js';
import type { Logger } from './logger/core.js';
@@ -118,10 +117,6 @@ export abstract class Pipeline {
// this middleware must be placed at the beginning because it needs to block incoming requests
internalMiddlewares.unshift(createOriginCheckMiddleware());
}
- if (this.manifest.shouldInjectCspMetaTags) {
- // this middleware must be placed at the end because it needs to inject the CSP headers
- internalMiddlewares.push(createCSPMiddleware());
- }
this.resolvedMiddleware = sequence(...internalMiddlewares);
return this.resolvedMiddleware;
} else {
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index 5e2c5485d287..48cc8efc07aa 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -27,8 +27,13 @@ import type {
SSRError,
SSRLoadedRenderer,
} from '../../types/public/internal.js';
-import type { SSRActions, SSRManifest, SSRManifestI18n } from '../app/types.js';
-import { shouldTrackCspHashes, trackScriptHashes, trackStyleHashes } from '../csp/common.js';
+import type { SSRActions, SSRManifestCSP, SSRManifest, SSRManifestI18n } from '../app/types.js';
+import {
+ getAlgorithm,
+ shouldTrackCspHashes,
+ trackScriptHashes,
+ trackStyleHashes,
+} from '../csp/common.js';
import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { NOOP_MIDDLEWARE_FN } from '../middleware/noop-middleware.js';
@@ -612,14 +617,7 @@ async function createBuildManifest(
key: Promise,
): Promise {
let i18nManifest: SSRManifestI18n | undefined = undefined;
-
- let clientStyleHashes: string[] = [];
- let clientScriptHashes: string[] = [];
-
- if (shouldTrackCspHashes(settings.config)) {
- clientScriptHashes = await trackScriptHashes(internals, settings);
- clientStyleHashes = await trackStyleHashes(internals, settings);
- }
+ let csp: SSRManifestCSP | undefined = undefined;
if (settings.config.i18n) {
i18nManifest = {
@@ -631,6 +629,18 @@ async function createBuildManifest(
domainLookupTable: {},
};
}
+
+ if (shouldTrackCspHashes(settings.config)) {
+ const algorithm = getAlgorithm(settings.config);
+ const clientScriptHashes = await trackScriptHashes(internals, settings, algorithm);
+ const clientStyleHashes = await trackStyleHashes(internals, settings, algorithm);
+
+ csp = {
+ clientStyleHashes,
+ clientScriptHashes,
+ algorithm,
+ };
+ }
return {
hrefRoot: settings.config.root.toString(),
srcDir: settings.config.srcDir,
@@ -664,8 +674,6 @@ async function createBuildManifest(
checkOrigin:
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
key,
- clientStyleHashes,
- clientScriptHashes,
- shouldInjectCspMetaTags: shouldTrackCspHashes(settings.config),
+ csp,
};
}
diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts
index 75fb1b466c0a..037a0e508e23 100644
--- a/packages/astro/src/core/build/plugins/plugin-manifest.ts
+++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts
@@ -12,7 +12,12 @@ import type {
SerializedRouteInfo,
SerializedSSRManifest,
} from '../../app/types.js';
-import { shouldTrackCspHashes, trackScriptHashes, trackStyleHashes } from '../../csp/common.js';
+import {
+ getAlgorithm,
+ shouldTrackCspHashes,
+ trackScriptHashes,
+ trackStyleHashes,
+} from '../../csp/common.js';
import { encodeKey } from '../../encryption.js';
import { fileExtension, joinPaths, prependForwardSlash } from '../../path.js';
import { DEFAULT_COMPONENTS } from '../../routing/default.js';
@@ -276,12 +281,18 @@ async function buildManifest(
};
}
- let clientScriptHashes: string[] = [];
- let clientStyleHashes: string[] = [];
+ let csp = undefined;
if (shouldTrackCspHashes(settings.config)) {
- clientScriptHashes = await trackScriptHashes(internals, opts.settings);
- clientStyleHashes = await trackStyleHashes(internals, opts.settings);
+ const algorithm = getAlgorithm(settings.config);
+ const clientScriptHashes = await trackScriptHashes(internals, opts.settings, algorithm);
+ const clientStyleHashes = await trackStyleHashes(internals, opts.settings, algorithm);
+
+ csp = {
+ clientStyleHashes,
+ clientScriptHashes,
+ algorithm,
+ };
}
return {
@@ -313,8 +324,6 @@ async function buildManifest(
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
key: encodedKey,
sessionConfig: settings.config.session,
- shouldInjectCspMetaTags: shouldTrackCspHashes(opts.settings.config),
- clientStyleHashes,
- clientScriptHashes,
+ csp,
};
}
diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts
index 5e7e30b07c63..42a00a8bdf73 100644
--- a/packages/astro/src/core/config/schemas/base.ts
+++ b/packages/astro/src/core/config/schemas/base.ts
@@ -11,6 +11,7 @@ import { z } from 'zod';
import { localFontFamilySchema, remoteFontFamilySchema } from '../../../assets/fonts/config.js';
import { EnvSchema } from '../../../env/schema.js';
import type { AstroUserConfig, ViteUserConfig } from '../../../types/public/config.js';
+import { cspAlgorithmSchema } from '../../csp/config.js';
// The below types are required boilerplate to workaround a Zod issue since v3.21.2. Since that version,
// Zod's compiled TypeScript would "simplify" certain values to their base representation, causing references
@@ -474,7 +475,15 @@ export const AstroConfigSchema = z.object({
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.preserveScriptOrder),
fonts: z.array(z.union([localFontFamilySchema, remoteFontFamilySchema])).optional(),
- csp: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.csp),
+ csp: z
+ .union([
+ z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.csp),
+ z.object({
+ algorithm: cspAlgorithmSchema,
+ }),
+ ])
+ .optional()
+ .default(ASTRO_CONFIG_DEFAULTS.experimental.csp),
})
.strict(
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/experimental-flags/ for a list of all current experiments.`,
diff --git a/packages/astro/src/core/config/schemas/index.ts b/packages/astro/src/core/config/schemas/index.ts
index f6a0289e7ae4..75689b0724c9 100644
--- a/packages/astro/src/core/config/schemas/index.ts
+++ b/packages/astro/src/core/config/schemas/index.ts
@@ -1,3 +1,7 @@
-export { AstroConfigSchema, ASTRO_CONFIG_DEFAULTS, type AstroConfigType } from './base.js';
+export {
+ AstroConfigSchema,
+ ASTRO_CONFIG_DEFAULTS,
+ type AstroConfigType,
+} from './base.js';
export { createRelativeSchema } from './relative.js';
export { AstroConfigRefinedSchema } from './refined.js';
diff --git a/packages/astro/src/core/csp/common.ts b/packages/astro/src/core/csp/common.ts
index d1ed301f7432..61dcb12fe247 100644
--- a/packages/astro/src/core/csp/common.ts
+++ b/packages/astro/src/core/csp/common.ts
@@ -4,23 +4,40 @@ import { ISLAND_STYLES } from '../../runtime/server/astro-island-styles.js';
import astroIslandPrebuiltDev from '../../runtime/server/astro-island.prebuilt-dev.js';
import astroIslandPrebuilt from '../../runtime/server/astro-island.prebuilt.js';
import type { AstroSettings } from '../../types/astro.js';
-import type { AstroConfig } from '../../types/public/index.js';
+import type { AstroConfig, CspAlgorithm } from '../../types/public/index.js';
import type { BuildInternals } from '../build/internal.js';
-import { generateDigest } from '../encryption.js';
+import { generateCspDigest } from '../encryption.js';
export function shouldTrackCspHashes(config: AstroConfig): boolean {
- return config.experimental?.csp === true;
+ return config.experimental?.csp === true || typeof config.experimental?.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) {
+ return 'SHA-256';
+ }
+ return config.experimental.csp.algorithm;
}
export async function trackStyleHashes(
internals: BuildInternals,
settings: AstroSettings,
+ algorithm: CspAlgorithm,
): Promise {
const clientStyleHashes: string[] = [];
for (const [_, page] of internals.pagesByViteID.entries()) {
for (const style of page.styles) {
if (style.sheet.type === 'inline') {
- clientStyleHashes.push(await generateDigest(style.sheet.content));
+ clientStyleHashes.push(await generateCspDigest(style.sheet.content, algorithm));
}
}
}
@@ -31,12 +48,12 @@ export async function trackStyleHashes(
'utf-8',
);
if (clientAsset.endsWith('.css') || clientAsset.endsWith('.css')) {
- clientStyleHashes.push(await generateDigest(contents));
+ clientStyleHashes.push(await generateCspDigest(contents, algorithm));
}
}
if (settings.renderers.length > 0) {
- clientStyleHashes.push(await generateDigest(ISLAND_STYLES));
+ clientStyleHashes.push(await generateCspDigest(ISLAND_STYLES, algorithm));
}
return clientStyleHashes;
@@ -45,15 +62,16 @@ export async function trackStyleHashes(
export async function trackScriptHashes(
internals: BuildInternals,
settings: AstroSettings,
+ algorithm: CspAlgorithm,
): Promise {
const clientScriptHashes: string[] = [];
for (const script of internals.inlinedScripts.values()) {
- clientScriptHashes.push(await generateDigest(script));
+ clientScriptHashes.push(await generateCspDigest(script, algorithm));
}
for (const directiveContent of Array.from(settings.clientDirectives.values())) {
- clientScriptHashes.push(await generateDigest(directiveContent));
+ clientScriptHashes.push(await generateCspDigest(directiveContent, algorithm));
}
for (const clientAsset in internals.clientChunksAndAssets) {
@@ -62,20 +80,20 @@ export async function trackScriptHashes(
'utf-8',
);
if (clientAsset.endsWith('.js') || clientAsset.endsWith('.mjs')) {
- clientScriptHashes.push(await generateDigest(contents));
+ clientScriptHashes.push(await generateCspDigest(contents, algorithm));
}
}
for (const script of settings.scripts) {
const { content, stage } = script;
if (stage === 'head-inline' || stage === 'before-hydration') {
- clientScriptHashes.push(await generateDigest(content));
+ clientScriptHashes.push(await generateCspDigest(content, algorithm));
}
}
if (settings.renderers.length > 0) {
- clientScriptHashes.push(await generateDigest(astroIslandPrebuilt));
- clientScriptHashes.push(await generateDigest(astroIslandPrebuiltDev));
+ clientScriptHashes.push(await generateCspDigest(astroIslandPrebuilt, algorithm));
+ clientScriptHashes.push(await generateCspDigest(astroIslandPrebuiltDev, algorithm));
}
return clientScriptHashes;
diff --git a/packages/astro/src/core/csp/config.ts b/packages/astro/src/core/csp/config.ts
new file mode 100644
index 000000000000..f3c61878326a
--- /dev/null
+++ b/packages/astro/src/core/csp/config.ts
@@ -0,0 +1,8 @@
+import { z } from 'zod';
+
+export const cspAlgorithmSchema = z
+ .enum(['SHA-512', 'SHA-384', 'SHA-256'])
+ .optional()
+ .default('SHA-256');
+
+export type CspAlgorithm = z.infer;
diff --git a/packages/astro/src/core/csp/middleware.ts b/packages/astro/src/core/csp/middleware.ts
deleted file mode 100644
index f05a8443b75f..000000000000
--- a/packages/astro/src/core/csp/middleware.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { MiddlewareHandler } from '../../types/public/index.js';
-
-export function createCSPMiddleware(): MiddlewareHandler {
- return async (_, next) => {
- const response = await next();
-
- // Do something with the response
-
- return response;
- };
-}
diff --git a/packages/astro/src/core/encryption.ts b/packages/astro/src/core/encryption.ts
index e3668ab5d865..d8fbfaa3f1a8 100644
--- a/packages/astro/src/core/encryption.ts
+++ b/packages/astro/src/core/encryption.ts
@@ -1,4 +1,5 @@
import { decodeBase64, decodeHex, encodeBase64, encodeHexUpperCase } from '@oslojs/encoding';
+import type { CspAlgorithm } from '../types/public/index.js';
// Chose this algorithm for no particular reason, can change.
// This algo does check against text manipulation though. See
@@ -113,9 +114,22 @@ export async function decryptString(key: CryptoKey, encoded: string) {
/**
* Generates an SHA-256 digest of the given string.
* @param {string} data The string to hash.
+ * @param {CspAlgorithm} algorithm The algorithm to use.
*/
-export async function generateDigest(data: string): Promise {
- const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(data));
+export async function generateCspDigest(data: string, algorithm: CspAlgorithm): Promise {
+ const hashBuffer = await crypto.subtle.digest(algorithm, encoder.encode(data));
- return encodeBase64(new Uint8Array(hashBuffer));
+ const hash = encodeBase64(new Uint8Array(hashBuffer));
+
+ switch (algorithm) {
+ case 'SHA-256': {
+ return `sha256-${hash}`;
+ }
+ case 'SHA-512': {
+ return `sha512-${hash}`;
+ }
+ case 'SHA-384': {
+ return `sha384-${hash}`;
+ }
+ }
}
diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts
index 4bed34435f9e..2b5ebe694baa 100644
--- a/packages/astro/src/core/render-context.ts
+++ b/packages/astro/src/core/render-context.ts
@@ -463,9 +463,10 @@ export class RenderContext {
extraScriptHashes: [],
propagators: new Set(),
},
- shouldInjectCspMetaTags: manifest.shouldInjectCspMetaTags,
- clientScriptHashes: manifest.clientScriptHashes,
- clientStyleHashes: manifest.clientStyleHashes,
+ shouldInjectCspMetaTags: !!manifest.csp,
+ clientScriptHashes: manifest.csp?.clientScriptHashes ?? [],
+ clientStyleHashes: manifest.csp?.clientStyleHashes ?? [],
+ cspAlgorithm: manifest.csp?.algorithm ?? 'SHA-256',
};
return result;
diff --git a/packages/astro/src/runtime/server/render/csp.ts b/packages/astro/src/runtime/server/render/csp.ts
index d7e1ad5dc73d..3f861e8ae925 100644
--- a/packages/astro/src/runtime/server/render/csp.ts
+++ b/packages/astro/src/runtime/server/render/csp.ts
@@ -5,19 +5,19 @@ export function renderCspContent(result: SSRResult): string {
const finalStyleHashes = new Set();
for (const scriptHash of result.clientScriptHashes) {
- finalScriptHashes.add(`'sha256-${scriptHash}'`);
+ finalScriptHashes.add(`'${scriptHash}'`);
}
for (const styleHash of result.clientStyleHashes) {
- finalStyleHashes.add(`'sha256-${styleHash}'`);
+ finalStyleHashes.add(`'${styleHash}'`);
}
for (const styleHash of result._metadata.extraStyleHashes) {
- finalStyleHashes.add(`'sha256-${styleHash}'`);
+ finalStyleHashes.add(`'${styleHash}'`);
}
for (const scriptHash of result._metadata.extraScriptHashes) {
- finalScriptHashes.add(`'sha256-${scriptHash}'`);
+ finalScriptHashes.add(`'${scriptHash}'`);
}
const scriptSrc = `style-src 'self' ${Array.from(finalStyleHashes).join(' ')};`;
diff --git a/packages/astro/src/runtime/server/render/server-islands.ts b/packages/astro/src/runtime/server/render/server-islands.ts
index 338da76bf890..8e830718e7af 100644
--- a/packages/astro/src/runtime/server/render/server-islands.ts
+++ b/packages/astro/src/runtime/server/render/server-islands.ts
@@ -1,4 +1,4 @@
-import { encryptString, generateDigest } from '../../../core/encryption.js';
+import { encryptString, generateCspDigest } from '../../../core/encryption.js';
import type { SSRResult } from '../../../types/public/internal.js';
import { markHTMLString } from '../escape.js';
import { renderChild } from './any.js';
@@ -137,8 +137,10 @@ let response = await fetch('${serverIslandUrl}', {
const content = `${method}replaceServerIsland('${hostId}', response);`;
if (this.result.shouldInjectCspMetaTags) {
- this.result._metadata.extraScriptHashes.push(await generateDigest(SERVER_ISLAND_REPLACER));
- const contentDigest = await generateDigest(content);
+ this.result._metadata.extraScriptHashes.push(
+ await generateCspDigest(SERVER_ISLAND_REPLACER, this.result.cspAlgorithm),
+ );
+ const contentDigest = await generateCspDigest(content, this.result.cspAlgorithm);
this.result._metadata.extraScriptHashes.push(contentDigest);
}
this.islandContent = content;
diff --git a/packages/astro/src/runtime/server/transition.ts b/packages/astro/src/runtime/server/transition.ts
index 92940c745a31..bae71248975b 100644
--- a/packages/astro/src/runtime/server/transition.ts
+++ b/packages/astro/src/runtime/server/transition.ts
@@ -1,5 +1,5 @@
import cssesc from 'cssesc';
-import { generateDigest } from '../../core/encryption.js';
+import { generateCspDigest } from '../../core/encryption.js';
import { fade, slide } from '../../transitions/index.js';
import type { SSRResult } from '../../types/public/internal.js';
import type {
@@ -113,7 +113,7 @@ export async function renderTransition(
const css = sheet.toString();
if (result.shouldInjectCspMetaTags) {
- result._metadata.extraStyleHashes.push(await generateDigest(css));
+ result._metadata.extraStyleHashes.push(await generateCspDigest(css, result.cspAlgorithm));
}
result._metadata.extraHead.push(markHTMLString(``));
return scope;
diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts
index 97816d8c0111..f7eee823fb09 100644
--- a/packages/astro/src/types/public/config.ts
+++ b/packages/astro/src/types/public/config.ts
@@ -18,11 +18,14 @@ 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';
export type Locales = (string | { codes: [string, ...string[]]; path: string })[];
export type { AstroFontProvider as FontProvider };
+export type { CspAlgorithm };
+
type NormalizeLocales = {
[K in keyof T]: T[K] extends string
? T[K]
@@ -2227,10 +2230,32 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
headingIdCompat?: boolean;
/**
+ * @name experimental.csp
+ * @type {boolean}
+ * @default `false`
+ * @version 5.5.x
+ * @description
+ *
+ * Enables built-in support for Content Security Policy (CSP).
*
*/
- // TODO: add docs once we are reaching the end
- csp?: boolean;
+ csp?:
+ | boolean
+ | {
+ /**
+ * @name experimental.csp.algorithm
+ * @type {string}
+ * @default `'SHA-256'`
+ * @version 5.5.x
+ * @description
+ *
+ * The hashing algorithm to use for the CSP.
+ *
+ * The default value is `'SHA-256'`.
+ *
+ */
+ algorithm?: CspAlgorithm;
+ };
/**
* @name experimental.preserveScriptOrder
diff --git a/packages/astro/src/types/public/internal.ts b/packages/astro/src/types/public/internal.ts
index 1f72b5b02625..e81e5ebfcb3a 100644
--- a/packages/astro/src/types/public/internal.ts
+++ b/packages/astro/src/types/public/internal.ts
@@ -1,7 +1,7 @@
// TODO: Should the types here really be public?
import type { ErrorPayload as ViteErrorPayload } from 'vite';
-import type { SSRManifest } from '../../core/app/types.js';
+import type { SSRManifestCSP } from '../../core/app/types.js';
import type { AstroCookies } from '../../core/cookies/cookies.js';
import type { AstroComponentInstance, ServerIslandComponent } from '../../runtime/server/index.js';
import type { Params } from './common.js';
@@ -251,8 +251,9 @@ export interface SSRResult {
* Whether Astro should inject the CSP tag into the head of the component.
*/
shouldInjectCspMetaTags: boolean;
- clientScriptHashes: SSRManifest['clientScriptHashes'];
- clientStyleHashes: SSRManifest['clientStyleHashes'];
+ cspAlgorithm: SSRManifestCSP['algorithm'];
+ clientScriptHashes: SSRManifestCSP['clientScriptHashes'];
+ clientStyleHashes: SSRManifestCSP['clientStyleHashes'];
}
/**
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index 567afee158f2..7deb62d13d63 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -4,8 +4,8 @@ import { IncomingMessage } from 'node:http';
import { fileURLToPath } from 'node:url';
import type * as vite from 'vite';
import { normalizePath } from 'vite';
-import type { SSRManifest, SSRManifestI18n } from '../core/app/types.js';
-import { shouldTrackCspHashes } from '../core/csp/common.js';
+import type { SSRManifestCSP, SSRManifest, SSRManifestI18n } from '../core/app/types.js';
+import { getAlgorithm, 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';
@@ -162,7 +162,8 @@ export default function createVitePluginAstroServer({
* @param settings
*/
export function createDevelopmentManifest(settings: AstroSettings): SSRManifest {
- let i18nManifest: SSRManifestI18n | undefined = undefined;
+ let i18nManifest: SSRManifestI18n | undefined;
+ let csp: SSRManifestCSP | undefined;
if (settings.config.i18n) {
i18nManifest = {
fallback: settings.config.i18n.fallback,
@@ -174,6 +175,14 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
};
}
+ if (shouldTrackCspHashes(settings.config)) {
+ csp = {
+ clientScriptHashes: [],
+ clientStyleHashes: [],
+ algorithm: getAlgorithm(settings.config),
+ };
+ }
+
return {
hrefRoot: settings.config.root.toString(),
srcDir: settings.config.srcDir,
@@ -207,8 +216,6 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
};
},
sessionConfig: settings.config.session,
- clientScriptHashes: [],
- clientStyleHashes: [],
- shouldInjectCspMetaTags: shouldTrackCspHashes(settings.config),
+ csp,
};
}
diff --git a/packages/astro/test/csp.test.js b/packages/astro/test/csp.test.js
index 2d2fcc2bba2c..3516b0f1f2ee 100644
--- a/packages/astro/test/csp.test.js
+++ b/packages/astro/test/csp.test.js
@@ -32,10 +32,10 @@ describe('CSP', () => {
const $ = cheerio.load(await response.text());
const meta = $('meta[http-equiv="Content-Security-Policy"]');
- for (const hash of manifest.clientStyleHashes) {
+ for (const hash of manifest.csp.clientStyleHashes) {
assert.match(
meta.attr('content'),
- new RegExp(`'sha256-${hash}'`),
+ new RegExp(`'${hash}'`),
`Should have a CSP meta tag for ${hash}`,
);
}
@@ -43,4 +43,63 @@ describe('CSP', () => {
assert.fail('Should have the manifest');
}
});
+ it('should generate the hash with the sha512 algorithm', async () => {
+ fixture = await loadFixture({
+ root: './fixtures/csp/',
+ adapter: testAdapter({
+ setManifest(_manifest) {
+ manifest = _manifest;
+ },
+ }),
+ experimental: {
+ csp: {
+ algorithm: 'SHA-512',
+ },
+ },
+ });
+ 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('sha512-'));
+ } else {
+ assert.fail('Should have the manifest');
+ }
+ });
+
+ it('should generate the hash with the sha384 algorithm', async () => {
+ fixture = await loadFixture({
+ root: './fixtures/csp/',
+ adapter: testAdapter({
+ setManifest(_manifest) {
+ manifest = _manifest;
+ },
+ }),
+ experimental: {
+ csp: {
+ algorithm: 'SHA-384',
+ },
+ },
+ });
+ 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-'));
+ } else {
+ assert.fail('Should have the manifest');
+ }
+ });
});