Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions packages/astro/src/actions/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export type ActionAPIContext = Pick<
| 'originPathname'
| 'session'
| 'insertDirective'
| 'insertScriptResource'
| 'insertStyleResource'
| 'insertScriptHash'
| 'insertStyleHash'
> & {
// TODO: remove in Astro 6.0
/**
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export type SSRManifestCSP = {
scriptResources: string[];
styleHashes: string[];
styleResources: string[];
directives: CspDirective;
directives: CspDirective[];
};

/** Public type exposed through the `astro:build:ssr` integration hook */
Expand Down
15 changes: 4 additions & 11 deletions 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 { ALLOWED_DIRECTIVES, cspAlgorithmSchema, CspHashSchema } from '../../csp/config.js';
import { allowedDirectivesSchema, cspAlgorithmSchema, cspHashSchema } 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 @@ -480,24 +480,17 @@ export const AstroConfigSchema = z.object({
z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.csp),
z.object({
algorithm: cspAlgorithmSchema,
directives: z
.array(
z.object({
type: z.enum(ALLOWED_DIRECTIVES),
value: z.string(),
}),
)
.optional(),
directives: z.array(allowedDirectivesSchema).optional(),
styleDirective: z
.object({
resources: z.array(z.string()).optional(),
hashes: z.array(CspHashSchema).optional(),
hashes: z.array(cspHashSchema).optional(),
})
.optional(),
scriptDirective: z
.object({
resources: z.array(z.string()).optional(),
hashes: z.array(CspHashSchema).optional(),
hashes: z.array(cspHashSchema).optional(),
})
.optional(),
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/csp/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function getStyleResources(csp: EnabledCsp): string[] {
return csp.styleDirective?.resources ?? [];
}

export function getDirectives(csp: EnabledCsp): CspDirective {
export function getDirectives(csp: EnabledCsp): CspDirective[] {
if (csp === true) {
return [];
}
Expand Down
22 changes: 14 additions & 8 deletions packages/astro/src/core/csp/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const cspAlgorithmSchema = z
.optional()
.default('SHA-256');

export const CspHashSchema = z.custom<`${CspAlgorithmValue}${string}`>((value) => {
export const cspHashSchema = z.custom<`${CspAlgorithmValue}${string}`>((value) => {
if (typeof value !== 'string') {
return false;
}
Expand All @@ -39,9 +39,9 @@ export const CspHashSchema = z.custom<`${CspAlgorithmValue}${string}`>((value) =
});
});

export type CspHash = z.infer<typeof CspHashSchema>;
export type CspHash = z.infer<typeof cspHashSchema>;

export const ALLOWED_DIRECTIVES = [
const ALLOWED_DIRECTIVES = [
'base-uri',
'child-src',
'connect-src',
Expand All @@ -64,9 +64,15 @@ export const ALLOWED_DIRECTIVES = [
'worker-src',
] as const;

type AllowedDirectives = (typeof ALLOWED_DIRECTIVES)[number];
export type CspDirective = `${AllowedDirectives} ${string}`;

export type CspDirective = {
type: AllowedDirectives;
value: string;
}[];
export const allowedDirectivesSchema = z.custom<CspDirective>((value) => {
if (typeof value !== 'string') {
return false;
}
return ALLOWED_DIRECTIVES.some((allowedValue) => {
return value.startsWith(allowedValue);
});
});

type AllowedDirectives = (typeof ALLOWED_DIRECTIVES)[number];
4 changes: 4 additions & 0 deletions packages/astro/src/core/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ function createContext({
throw new AstroError(AstroErrorData.LocalsReassigned);
},
insertDirective() {},
insertScriptResource() {},
insertStyleResource() {},
insertScriptHash() {},
insertStyleHash() {},
};
return Object.assign(context, {
getActionResult: createGetActionResult(context.locals),
Expand Down
80 changes: 67 additions & 13 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import { copyRequest, getOriginPathname, setOriginPathname } from './routing/rew
import { AstroSession } from './session.js';

export const apiContextRoutesSymbol = Symbol.for('context.routes');

/**
* Each request is rendered using a `RenderContext`.
* It contains data unique to each request. It is responsible for executing middleware, calling endpoints, and rendering the page by gathering necessary data from a `Pipeline`.
Expand Down Expand Up @@ -73,6 +72,8 @@ export class RenderContext {
*/
counter = 0;

result: SSRResult | undefined = undefined;

static async create({
locals = {},
middleware,
Expand Down Expand Up @@ -223,10 +224,10 @@ export class RenderContext {
case 'redirect':
return renderRedirect(this);
case 'page': {
const result = await this.createResult(componentInstance!, actionApiContext);
this.result = await this.createResult(componentInstance!, actionApiContext);
try {
response = await renderPage(
result,
this.result,
componentInstance?.default as any,
props,
slots,
Expand All @@ -236,7 +237,7 @@ export class RenderContext {
} catch (e) {
// If there is an error in the page's frontmatter or instantiation of the RenderTemplate fails midway,
// we signal to the rest of the internals that we can ignore the results of existing renders and avoid kicking off more of them.
result.cancelled = true;
this.result.cancelled = true;
throw e;
}

Expand Down Expand Up @@ -394,11 +395,37 @@ export class RenderContext {
}
return renderContext.session;
},
insertDirective(_payload) {
insertDirective(payload) {
if (!!pipeline.manifest.csp === false) {
Comment thread
ematipico marked this conversation as resolved.
Outdated
throw new AstroError(CspNotEnabled);
}
renderContext.result?.directives.push(payload);
},

insertScriptResource(resource) {
if (!!pipeline.manifest.csp === false) {
Comment thread
ematipico marked this conversation as resolved.
Outdated
throw new AstroError(CspNotEnabled);
}
renderContext.result?.scriptResources.push(resource);
},
insertStyleResource(resource) {
if (!!pipeline.manifest.csp === false) {
Comment thread
ematipico marked this conversation as resolved.
Outdated
throw new AstroError(CspNotEnabled);
}

renderContext.result?.styleResources.push(resource);
},
insertStyleHash(hash) {
if (!!pipeline.manifest.csp === false) {
Comment thread
ematipico marked this conversation as resolved.
Outdated
throw new AstroError(CspNotEnabled);
}
renderContext.result?.styleHashes.push(hash);
},
insertScriptHash(hash) {
if (!!pipeline.manifest.csp === false) {
throw new AstroError(CspNotEnabled);
}
// TODO: add the directive
renderContext.result?.scriptHashes.push(hash);
},
};
}
Expand Down Expand Up @@ -470,12 +497,13 @@ export class RenderContext {
propagators: new Set(),
},
shouldInjectCspMetaTags: !!manifest.csp,
scriptHashes: manifest.csp?.scriptHashes ?? [],
scriptResources: manifest.csp?.scriptResources ?? [],
styleHashes: manifest.csp?.styleHashes ?? [],
styleResources: manifest.csp?.styleResources ?? [],
cspAlgorithm: manifest.csp?.algorithm ?? 'SHA-256',
directives: manifest.csp?.directives ?? [],
// The following arrays must be cloned, otherwise they become mutable across routes.
scriptHashes: !!manifest.csp?.scriptHashes ? [...manifest.csp.scriptHashes] : [],
scriptResources: !!manifest.csp?.scriptResources ? [...manifest.csp.scriptResources] : [],
styleHashes: !!manifest.csp?.styleHashes ? [...manifest.csp.styleHashes] : [],
styleResources: !!manifest.csp?.styleResources ? [...manifest.csp.styleResources] : [],
directives: !!manifest.csp?.directives ? [...manifest.csp.directives] : [],
Comment thread
ematipico marked this conversation as resolved.
Outdated
};

return result;
Expand Down Expand Up @@ -615,11 +643,37 @@ export class RenderContext {
get originPathname() {
return getOriginPathname(renderContext.request);
},
insertDirective(_payload) {
insertDirective(payload) {
if (!!pipeline.manifest.csp === false) {
Comment thread
ematipico marked this conversation as resolved.
Outdated
throw new AstroError(CspNotEnabled);
}
renderContext.result?.directives.push(payload);
},

insertScriptResource(resource) {
if (!!pipeline.manifest.csp === false) {
Comment thread
ematipico marked this conversation as resolved.
Outdated
throw new AstroError(CspNotEnabled);
}
renderContext.result?.scriptResources.push(resource);
},
insertStyleResource(resource) {
if (!!pipeline.manifest.csp === false) {
Comment thread
ematipico marked this conversation as resolved.
Outdated
throw new AstroError(CspNotEnabled);
}

renderContext.result?.styleResources.push(resource);
},
insertStyleHash(hash) {
if (!!pipeline.manifest.csp === false) {
Comment thread
ematipico marked this conversation as resolved.
Outdated
throw new AstroError(CspNotEnabled);
}
renderContext.result?.styleHashes.push(hash);
},
insertScriptHash(hash) {
if (!!pipeline.manifest.csp === false) {
throw new AstroError(CspNotEnabled);
}
// TODO: add the directive
renderContext.result?.scriptHashes.push(hash);
},
};
}
Expand Down
10 changes: 5 additions & 5 deletions packages/astro/src/runtime/server/render/csp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ 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(';');

let directives = '';
if (result.directives.length > 0) {
directives = result.directives.join(';') + ';';
}

let scriptResources = "'self'";
if (result.scriptResources.length > 0) {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2357,7 +2357,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
* ```
*
*/
directives?: CspDirective;
directives?: CspDirective[];
};

/**
Expand Down
55 changes: 52 additions & 3 deletions packages/astro/src/types/public/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,59 @@ export interface AstroSharedContext<
isPrerendered: boolean;

/**
* When CSP is enabled, it allows
* @param payload
* It adds a specific CSP directive to the route being rendered.
*
* ## Example
*
* ```js
* ctx.insertDirective("default-src 'self' 'unsafe-inline' https://example.com")
* ```
*/
insertDirective: (directive: CspDirective) => void;

/**
* It set the resource for the directive `style-src` in the route being rendered. It overrides Astro's default.
*
* ## Example
*
* ```js
* ctx.insertStyleResource("https://styles.cdn.example.com/")
* ```
*/
insertStyleResource: (payload: string) => void;

/**
* Insert a single style hash to the route being rendered.
*
* ## Example
*
* ```js
* ctx.insertStyleHash("sha256-1234567890abcdef1234567890")
* ```
*/
insertStyleHash: (hash: string) => void;

/**
* It set the resource for the directive `script-src` in the route being rendered.
*
* ## Example
*
* ```js
* ctx.insertScriptResource("https://scripts.cdn.example.com/")
* ```
*/
insertScriptResource: (resource: string) => void;

/**
* Insert a single script hash to the route being rendered.
*
* ## Example
*
* ```js
* ctx.insertScriptHash("sha256-1234567890abcdef1234567890")
* ```
*/
insertDirective: (payload: CspDirective) => void;
insertScriptHash: (hash: string) => void;
}

/**
Expand Down
Loading