Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions packages/astro/src/actions/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type ActionAPIContext = Pick<
| 'preferredLocaleList'
| 'originPathname'
| 'session'
| 'insertDirective'
> & {
// TODO: remove in Astro 6.0
/**
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
SSRResult,
} from '../../types/public/internal.js';
import type { SinglePageBuiltModule } from '../build/types.js';
import type { CspDirective } from '../csp/config.js';

type ComponentPath = string;

Expand Down Expand Up @@ -111,6 +112,7 @@ export type SSRManifestCSP = {
algorithm: CspAlgorithm;
clientScriptHashes: string[];
clientStyleHashes: string[];
directives: CspDirective;
};

/** Public type exposed through the `astro:build:ssr` integration hook */
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
getAlgorithm,
getScriptHashes,
getStyleHashes,
getDirectives,
shouldTrackCspHashes,
trackScriptHashes,
trackStyleHashes,
Expand Down Expand Up @@ -647,6 +648,7 @@ async function createBuildManifest(
clientStyleHashes,
clientScriptHashes,
algorithm,
directives: getDirectives(settings.config.experimental.csp),
};
}
return {
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getAlgorithm,
getScriptHashes,
getStyleHashes,
getDirectives,
shouldTrackCspHashes,
trackScriptHashes,
trackStyleHashes,
Expand Down Expand Up @@ -300,6 +301,7 @@ async function buildManifest(
clientStyleHashes,
clientScriptHashes,
algorithm,
directives: getDirectives(settings.config.experimental.csp),
};
}

Expand Down
10 changes: 9 additions & 1 deletion packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +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';
import { ALLOWED_DIRECTIVES, 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 @@ -482,6 +482,14 @@ export const AstroConfigSchema = z.object({
algorithm: cspAlgorithmSchema,
styleHashes: z.array(z.string()).optional(),
scriptHashes: z.array(z.string()).optional(),
directives: z
.array(
z.object({
type: z.enum(ALLOWED_DIRECTIVES),
value: z.string(),
}),
)
.optional(),
}),
])
.optional()
Expand Down
34 changes: 18 additions & 16 deletions packages/astro/src/core/config/schemas/refined.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,28 +209,30 @@ export const AstroConfigRefinedSchema = z.custom<AstroConfig>().superRefine((con
const { scriptHashes, styleHashes } = config.experimental.csp;
if (scriptHashes) {
for (const hash of 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'],
});
}
const allowed = ALGORITHM_VALUES.some((allowedValue) => {
return hash.startsWith(allowedValue);
});
if (!allowed) {
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) {
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'],
});
}
const allowed = ALGORITHM_VALUES.some((allowedValue) => {
return hash.startsWith(allowedValue);
});
if (!allowed) {
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'],
});
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/astro/src/core/csp/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { AstroSettings } from '../../types/astro.js';
import type { AstroConfig, CspAlgorithm } from '../../types/public/index.js';
import type { BuildInternals } from '../build/internal.js';
import { generateCspDigest } from '../encryption.js';
import type { CspDirective } from './config.js';

type EnabledCsp = Exclude<AstroConfig['experimental']['csp'], false>;

Expand Down Expand Up @@ -37,6 +38,13 @@ export function getStyleHashes(csp: EnabledCsp): string[] {
}
}

export function getDirectives(csp: EnabledCsp): CspDirective {
if (csp === true) {
return [];
}
return csp.directives ?? [];
}

export async function trackStyleHashes(
internals: BuildInternals,
settings: AstroSettings,
Expand Down
30 changes: 30 additions & 0 deletions packages/astro/src/core/csp/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,33 @@ export const cspAlgorithmSchema = z
.enum(Object.keys(ALGORITHMS) as UnionToTuple<CspAlgorithm>)
.optional()
.default('SHA-256');

export const ALLOWED_DIRECTIVES = [
'base-uri',
'child-src',
'connect-src',
'default-src',
'fenced-frame-src',
'font-src',
'form-action',
'frame-ancestors',
'frame-src',
'img-src',
'manifest-src',
'media-src',
'object-src',
'referrer',
'report-to',
'require-trusted-types-for',
'sandbox',
'trusted-types',
'upgrade-insecure-requests',
'worker-src',
] as const;

export type AllowedDirectives = (typeof ALLOWED_DIRECTIVES)[number];

export type CspDirective = {
type: AllowedDirectives;
value: string;
}[];
13 changes: 13 additions & 0 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1380,6 +1380,19 @@ export const FontFamilyNotFound = {
hint: 'This is often caused by a typo. Check that your Font component is using a `cssVariable` specified in your config.',
} satisfies ErrorData;

/**
* @docs
* @description
* The CSP feature isn't enabled
* @message
* The `experimental.csp` configuration isn't enabled.
*/
export const CspNotEnabled = {
name: 'CspNotEnabled',
title: "CSP feature isn't enabled",
message: "The `experimental.csp` configuration isn't enabled.",
} satisfies ErrorData;

/**
* @docs
* @kind heading
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ function createContext({
set locals(_) {
throw new AstroError(AstroErrorData.LocalsReassigned);
},
insertDirective() {},
};
return Object.assign(context, {
getActionResult: createGetActionResult(context.locals),
Expand Down
15 changes: 14 additions & 1 deletion packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from './constants.js';
import { AstroCookies, attachCookiesToResponse } from './cookies/index.js';
import { getCookiesFromResponse } from './cookies/response.js';
import { ForbiddenRewrite } from './errors/errors-data.js';
import { CspNotEnabled, ForbiddenRewrite } from './errors/errors-data.js';
import { AstroError, AstroErrorData } from './errors/index.js';
import { callMiddleware } from './middleware/callMiddleware.js';
import { sequence } from './middleware/index.js';
Expand Down Expand Up @@ -394,6 +394,12 @@ export class RenderContext {
}
return renderContext.session;
},
insertDirective(_payload) {
if (!!pipeline.manifest.csp === false) {
throw new AstroError(CspNotEnabled);
}
// TODO: add the directive
},
};
}

Expand Down Expand Up @@ -467,6 +473,7 @@ export class RenderContext {
clientScriptHashes: manifest.csp?.clientScriptHashes ?? [],
clientStyleHashes: manifest.csp?.clientStyleHashes ?? [],
cspAlgorithm: manifest.csp?.algorithm ?? 'SHA-256',
directives: manifest.csp?.directives ?? [],
};

return result;
Expand Down Expand Up @@ -606,6 +613,12 @@ export class RenderContext {
get originPathname() {
return getOriginPathname(renderContext.request);
},
insertDirective(_payload) {
if (!!pipeline.manifest.csp === false) {
throw new AstroError(CspNotEnabled);
}
// TODO: add the directive
},
};
}

Expand Down
8 changes: 6 additions & 2 deletions packages/astro/src/runtime/server/render/csp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ export function renderCspContent(result: SSRResult): string {
for (const scriptHash of result._metadata.extraScriptHashes) {
finalScriptHashes.add(`'${scriptHash}'`);
}

const directives = result.directives
.map(({ type, value }) => {
return `${type} ${value}`;
})
.join(';');
const scriptSrc = `style-src 'self' ${Array.from(finalStyleHashes).join(' ')};`;
const styleSrc = `script-src 'self' ${Array.from(finalScriptHashes).join(' ')};`;
return `${scriptSrc} ${styleSrc}`;
return `${directives} ${scriptSrc} ${styleSrc}`;
}
33 changes: 32 additions & 1 deletion packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, CspAlgorithmValue } from '../../core/csp/config.js';
import type { CspAlgorithm, CspAlgorithmValue, CspDirective } from '../../core/csp/config.js';

export type Locales = (string | { codes: [string, ...string[]]; path: string })[];

Expand Down Expand Up @@ -2283,6 +2283,37 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
*
*/
scriptHashes?: `${CspAlgorithmValue}${string}`[];

/**
* @name experimental.csp.directives
* @type {string[]}
* @default `[]`
* @version 5.5.x
* @description
*
* An array of additional directives to add the content of the `Content-Security-Policy` `<meta>` element.
*
* Use this configuration to add other directive definitions such as `default-src`, `image-src`, etc.
*
* ##### Example
*
* You can define a directive to fetch images only from a CDN `cdn.example.com`.
*
* ```js
* export default defineConfig({
* experimental: {
* csp: {
* directives: [{
* type: "image-src"
* content: 'https://cdn.example.com'"
* }]
* }
* }
* })
* ```
*
*/
directives?: CspDirective;
};

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/types/public/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { AstroComponentFactory } from '../../runtime/server/index.js';
import type { Params, RewritePayload } from './common.js';
import type { ValidRedirectStatus } from './config.js';
import type { AstroInstance, MDXInstance, MarkdownInstance } from './content.js';
import type { CspDirective } from '../../core/csp/config.js';

/**
* Astro global available in all contexts in .astro files
Expand Down Expand Up @@ -354,6 +355,12 @@ export interface AstroSharedContext<
* Whether the current route is prerendered or not.
*/
isPrerendered: boolean;

/**
* When CSP is enabled, it allows
* @param payload
*/
insertDirective: (payload: CspDirective) => void;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/types/public/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export interface SSRResult {
cspAlgorithm: SSRManifestCSP['algorithm'];
clientScriptHashes: SSRManifestCSP['clientScriptHashes'];
clientStyleHashes: SSRManifestCSP['clientStyleHashes'];
directives: SSRManifestCSP['directives'];
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/vite-plugin-astro-server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getScriptHashes,
getStyleHashes,
shouldTrackCspHashes,
getDirectives
} from '../core/csp/common.js';
import { warnMissingAdapter } from '../core/dev/adapter-validation.js';
import { createKey, getEnvironmentKey, hasEnvironmentKey } from '../core/encryption.js';
Expand Down Expand Up @@ -185,6 +186,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
clientScriptHashes: getScriptHashes(settings.config.experimental.csp),
clientStyleHashes: getStyleHashes(settings.config.experimental.csp),
algorithm: getAlgorithm(settings.config.experimental.csp),
directives: getDirectives(settings.config.experimental.csp),
};
}

Expand Down
Loading
Loading