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
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ export default defineConfig({
build: {
assetsInlineLimit: 0,
},
},
}
});
6 changes: 4 additions & 2 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,10 @@ export type SSRManifestI18n = {

export type SSRManifestCSP = {
algorithm: CspAlgorithm;
clientScriptHashes: string[];
clientStyleHashes: string[];
scriptHashes: string[];
scriptResources: string[];
styleHashes: string[];
styleResources: string[];
directives: CspDirective;
};

Expand Down
12 changes: 8 additions & 4 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
shouldTrackCspHashes,
trackScriptHashes,
trackStyleHashes,
getScriptResources,
getStyleResources,
} from '../csp/common.js';
import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
Expand Down Expand Up @@ -635,18 +637,20 @@ async function createBuildManifest(

if (shouldTrackCspHashes(settings.config.experimental.csp)) {
const algorithm = getAlgorithm(settings.config.experimental.csp);
const clientScriptHashes = [
const scriptHashes = [
...getScriptHashes(settings.config.experimental.csp),
...(await trackScriptHashes(internals, settings, algorithm)),
];
const clientStyleHashes = [
const styleHashes = [
...getStyleHashes(settings.config.experimental.csp),
...(await trackStyleHashes(internals, settings, algorithm)),
];

csp = {
clientStyleHashes,
clientScriptHashes,
styleHashes,
styleResources: getStyleResources(settings.config.experimental.csp),
scriptHashes,
scriptResources: getScriptResources(settings.config.experimental.csp),
algorithm,
directives: getDirectives(settings.config.experimental.csp),
};
Expand Down
15 changes: 10 additions & 5 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
SSRManifestI18n,
SerializedRouteInfo,
SerializedSSRManifest,
SSRManifestCSP,
} from '../../app/types.js';
import {
getAlgorithm,
Expand All @@ -20,6 +21,8 @@ import {
shouldTrackCspHashes,
trackScriptHashes,
trackStyleHashes,
getScriptResources,
getStyleResources,
} from '../../csp/common.js';
import { encodeKey } from '../../encryption.js';
import { fileExtension, joinPaths, prependForwardSlash } from '../../path.js';
Expand Down Expand Up @@ -284,22 +287,24 @@ async function buildManifest(
};
}

let csp = undefined;
let csp: SSRManifestCSP | undefined = undefined;

if (shouldTrackCspHashes(settings.config.experimental.csp)) {
const algorithm = getAlgorithm(settings.config.experimental.csp);
const clientScriptHashes = [
const scriptHashes = [
...getScriptHashes(settings.config.experimental.csp),
...(await trackScriptHashes(internals, settings, algorithm)),
];
const clientStyleHashes = [
const styleHashes = [
...getStyleHashes(settings.config.experimental.csp),
...(await trackStyleHashes(internals, settings, algorithm)),
];

csp = {
clientStyleHashes,
clientScriptHashes,
scriptHashes,
scriptResources: getScriptResources(settings.config.experimental.csp),
styleHashes,
styleResources: getStyleResources(settings.config.experimental.csp),
algorithm,
directives: getDirectives(settings.config.experimental.csp),
};
Expand Down
16 changes: 13 additions & 3 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 } from '../../csp/config.js';
import { ALLOWED_DIRECTIVES, 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,8 +480,6 @@ export const AstroConfigSchema = z.object({
z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.csp),
z.object({
algorithm: cspAlgorithmSchema,
styleHashes: z.array(z.string()).optional(),
scriptHashes: z.array(z.string()).optional(),
directives: z
.array(
z.object({
Expand All @@ -490,6 +488,18 @@ export const AstroConfigSchema = z.object({
}),
)
.optional(),
styleDirective: z
.object({
resources: z.array(z.string()).optional(),
hashes: z.array(CspHashSchema).optional(),
})
.optional(),
scriptDirective: z
.object({
resources: z.array(z.string()).optional(),
hashes: z.array(CspHashSchema).optional(),
})
.optional(),
}),
])
.optional()
Expand Down
34 changes: 0 additions & 34 deletions packages/astro/src/core/config/schemas/refined.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { z } from 'zod';
import type { AstroConfig } from '../../../types/public/config.js';
import { ALGORITHM_VALUES } from '../../csp/config.js';

export const AstroConfigRefinedSchema = z.custom<AstroConfig>().superRefine((config, ctx) => {
if (
Expand Down Expand Up @@ -204,37 +203,4 @@ export const AstroConfigRefinedSchema = z.custom<AstroConfig>().superRefine((con
}
}
}

if (config.experimental.csp && typeof config.experimental.csp === 'object') {
const { scriptHashes, styleHashes } = config.experimental.csp;
if (scriptHashes) {
for (const hash of 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) {
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'],
});
}
}
}
}
});
19 changes: 16 additions & 3 deletions packages/astro/src/core/csp/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,29 @@ export function getScriptHashes(csp: EnabledCsp): string[] {
if (csp === true) {
return [];
} else {
return csp.scriptHashes ?? [];
return csp.scriptDirective?.hashes ?? [];
}
}

export function getScriptResources(csp: EnabledCsp): string[] {
if (csp === true) {
return [];
}
return csp.scriptDirective?.resources ?? [];
}

export function getStyleHashes(csp: EnabledCsp): string[] {
if (csp === true) {
return [];
} else {
return csp.styleHashes ?? [];
}
return csp.styleDirective?.hashes ?? [];
}

export function getStyleResources(csp: EnabledCsp): string[] {
if (csp === true) {
return [];
}
return csp.styleDirective?.resources ?? [];
}

export function getDirectives(csp: EnabledCsp): CspDirective {
Expand Down
17 changes: 14 additions & 3 deletions packages/astro/src/core/csp/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type UnionToTuple<T> = UnionToIntersection<T extends never ? never : (t: T) => T
? [...UnionToTuple<Exclude<T, W>>, W]
: [];

const ALGORITHMS = {
export const ALGORITHMS = {
'SHA-256': 'sha256-',
'SHA-384': 'sha384-',
'SHA-512': 'sha512-',
Expand All @@ -21,15 +21,26 @@ const ALGORITHMS = {
type Algorithms = typeof ALGORITHMS;

export type CspAlgorithm = keyof Algorithms;
export type CspAlgorithmValue = Algorithms[keyof Algorithms];
type CspAlgorithmValue = Algorithms[keyof Algorithms];

export const ALGORITHM_VALUES = Object.values(ALGORITHMS) as UnionToTuple<CspAlgorithmValue>;
const ALGORITHM_VALUES = Object.values(ALGORITHMS) as UnionToTuple<CspAlgorithmValue>;

export const cspAlgorithmSchema = z
.enum(Object.keys(ALGORITHMS) as UnionToTuple<CspAlgorithm>)
.optional()
.default('SHA-256');

export const CspHashSchema = z.custom<`${CspAlgorithmValue}${string}`>((value) => {
if (typeof value !== 'string') {
return false;
}
return ALGORITHM_VALUES.some((allowedValue) => {
return value.startsWith(allowedValue);
});
});

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

export const ALLOWED_DIRECTIVES = [
'base-uri',
'child-src',
Expand Down
16 changes: 3 additions & 13 deletions packages/astro/src/core/encryption.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { decodeBase64, decodeHex, encodeBase64, encodeHexUpperCase } from '@oslojs/encoding';
import type { CspAlgorithm } from '../types/public/index.js';
import { ALGORITHMS, type CspHash } from './csp/config.js';

// Chose this algorithm for no particular reason, can change.
// This algo does check against text manipulation though. See
Expand Down Expand Up @@ -116,20 +117,9 @@ export async function decryptString(key: CryptoKey, encoded: string) {
* @param {string} data The string to hash.
* @param {CspAlgorithm} algorithm The algorithm to use.
*/
export async function generateCspDigest(data: string, algorithm: CspAlgorithm): Promise<string> {
export async function generateCspDigest(data: string, algorithm: CspAlgorithm): Promise<CspHash> {
const hashBuffer = await crypto.subtle.digest(algorithm, encoder.encode(data));

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}`;
}
}
return `${ALGORITHMS[algorithm]}${hash}`;
}
6 changes: 4 additions & 2 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,8 +470,10 @@ export class RenderContext {
propagators: new Set(),
},
shouldInjectCspMetaTags: !!manifest.csp,
clientScriptHashes: manifest.csp?.clientScriptHashes ?? [],
clientStyleHashes: manifest.csp?.clientStyleHashes ?? [],
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 ?? [],
};
Expand Down
19 changes: 15 additions & 4 deletions packages/astro/src/runtime/server/render/csp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ export function renderCspContent(result: SSRResult): string {
const finalScriptHashes = new Set();
const finalStyleHashes = new Set();

for (const scriptHash of result.clientScriptHashes) {
for (const scriptHash of result.scriptHashes) {
finalScriptHashes.add(`'${scriptHash}'`);
}

for (const styleHash of result.clientStyleHashes) {
for (const styleHash of result.styleHashes) {
finalStyleHashes.add(`'${styleHash}'`);
}

Expand All @@ -24,7 +24,18 @@ export function renderCspContent(result: SSRResult): string {
return `${type} ${value}`;
})
.join(';');
const scriptSrc = `style-src 'self' ${Array.from(finalStyleHashes).join(' ')};`;
const styleSrc = `script-src 'self' ${Array.from(finalScriptHashes).join(' ')};`;

let scriptResources = "'self'";
if (result.scriptResources.length > 0) {
scriptResources = result.scriptResources.map((r) => `'${r}'`).join(' ');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there scenarios where this could need escaping?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I'm aware of. URLs don't have apostrophes, and the other resources are well-known

}

let styleResources = "'self'";
if (result.styleResources.length > 0) {
styleResources = result.styleResources.map((r) => `'${r}'`).join(' ');
}

const scriptSrc = `style-src ${styleResources} ${Array.from(finalStyleHashes).join(' ')};`;
const styleSrc = `script-src ${scriptResources} ${Array.from(finalScriptHashes).join(' ')};`;
return `${directives} ${scriptSrc} ${styleSrc}`;
}
Loading
Loading