Skip to content
Merged
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
8 changes: 2 additions & 6 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -249,9 +247,7 @@ type AstroContainerManifest = Pick<
| 'publicDir'
| 'outDir'
| 'cacheDir'
| 'clientScriptHashes'
| 'clientStyleHashes'
| 'shouldInjectCspMetaTags'
| 'csp'
>;

type AstroContainerConstructor = {
Expand Down
20 changes: 13 additions & 7 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 `<meta>` tag
*/
shouldInjectCspMetaTags: boolean;
csp: SSRManifestCSP | undefined;
};

export type SSRActions = {
Expand All @@ -107,6 +107,12 @@ export type SSRManifestI18n = {
domainLookupTable: Record<string, string>;
};

export type SSRManifestCSP = {
algorithm: CspAlgorithm;
clientScriptHashes: string[];
clientStyleHashes: string[];
};

/** Public type exposed through the `astro:build:ssr` integration hook */
export type SerializedSSRManifest = Omit<
SSRManifest,
Expand Down
5 changes: 0 additions & 5 deletions packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
34 changes: 21 additions & 13 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -612,14 +617,7 @@ async function createBuildManifest(
key: Promise<CryptoKey>,
): Promise<SSRManifest> {
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 = {
Expand All @@ -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,
Expand Down Expand Up @@ -664,8 +674,6 @@ async function createBuildManifest(
checkOrigin:
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
key,
clientStyleHashes,
clientScriptHashes,
shouldInjectCspMetaTags: shouldTrackCspHashes(settings.config),
csp,
};
}
25 changes: 17 additions & 8 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
};
}
11 changes: 10 additions & 1 deletion packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.`,
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/src/core/config/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -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';
42 changes: 30 additions & 12 deletions packages/astro/src/core/csp/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]> {
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));
}
}
}
Expand All @@ -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;
Expand All @@ -45,15 +62,16 @@ export async function trackStyleHashes(
export async function trackScriptHashes(
internals: BuildInternals,
settings: AstroSettings,
algorithm: CspAlgorithm,
): Promise<string[]> {
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) {
Expand All @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions packages/astro/src/core/csp/config.ts
Original file line number Diff line number Diff line change
@@ -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<typeof cspAlgorithmSchema>;
11 changes: 0 additions & 11 deletions packages/astro/src/core/csp/middleware.ts

This file was deleted.

20 changes: 17 additions & 3 deletions packages/astro/src/core/encryption.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<string> {
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(data));
export async function generateCspDigest(data: string, algorithm: CspAlgorithm): Promise<string> {
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}`;
}
}
}
7 changes: 4 additions & 3 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading