Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
24 changes: 17 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,16 @@ export type SSRManifestI18n = {
domainLookupTable: Record<string, string>;
};

export type SSRManifestCSP = {
algorithm: CspAlgorithm;
clientScriptHashes: string[];
clientStyleHashes: string[];
/**
* When enabled, Astro tracks the hashes of script and styles, and eventually it will render the `<meta>` tag
*/
shouldInjectCspMetaTags: boolean;
};

/** 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
35 changes: 22 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,19 @@ 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,
shouldInjectCspMetaTags: shouldTrackCspHashes(settings.config),
algorithm
};
}
return {
hrefRoot: settings.config.root.toString(),
srcDir: settings.config.srcDir,
Expand Down Expand Up @@ -664,8 +675,6 @@ async function createBuildManifest(
checkOrigin:
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
key,
clientStyleHashes,
clientScriptHashes,
shouldInjectCspMetaTags: shouldTrackCspHashes(settings.config),
csp,
};
}
26 changes: 18 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,19 @@ 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 = {
shouldInjectCspMetaTags: shouldTrackCspHashes(opts.settings.config),
clientStyleHashes,
clientScriptHashes,
algorithm,
};
}

return {
Expand Down Expand Up @@ -313,8 +325,6 @@ async function buildManifest(
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
key: encodedKey,
sessionConfig: settings.config.session,
shouldInjectCspMetaTags: shouldTrackCspHashes(opts.settings.config),
clientStyleHashes,
clientScriptHashes,
csp,
};
}
14 changes: 13 additions & 1 deletion packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ const highlighterTypesSchema = z
.union([z.literal('shiki'), z.literal('prism')])
.default(syntaxHighlightDefaults.type);

const cspAlgorithmSchema = z.enum(['SHA-512', 'SHA-384', 'SHA-256']).optional().default('SHA-256');

export const AstroConfigSchema = z.object({
root: z
.string()
Expand Down Expand Up @@ -474,7 +476,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 All @@ -487,4 +497,6 @@ export const AstroConfigSchema = z.object({
.default({}),
});

export type CspAlgorithm = z.infer<typeof cspAlgorithmSchema>;

export type AstroConfigType = z.infer<typeof AstroConfigSchema>;
7 changes: 6 additions & 1 deletion packages/astro/src/core/config/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export { AstroConfigSchema, ASTRO_CONFIG_DEFAULTS, type AstroConfigType } from './base.js';
export {
AstroConfigSchema,
ASTRO_CONFIG_DEFAULTS,
type AstroConfigType,
type CspAlgorithm,
} from './base.js';
export { createRelativeSchema } from './relative.js';
export { AstroConfigRefinedSchema } from './refined.js';
40 changes: 28 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,38 @@ 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) {
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 +46,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 +60,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 +78,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
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 {'sha256' | 'sha512' | 'sha384'} 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> {
let 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?.shouldInjectCspMetaTags ?? false,
clientScriptHashes: manifest.csp?.clientScriptHashes ?? [],
clientStyleHashes: manifest.csp?.clientStyleHashes ?? [],
cspAlgorithm: manifest.csp?.algorithm ?? 'SHA-256',
};

return result;
Expand Down
Loading
Loading