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
16 changes: 12 additions & 4 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import type {
import type { SSRActions, SSRManifestCSP, SSRManifest, SSRManifestI18n } from '../app/types.js';
import {
getAlgorithm,
getScriptHashes,
getStyleHashes,
shouldTrackCspHashes,
trackScriptHashes,
trackStyleHashes,
Expand Down Expand Up @@ -630,10 +632,16 @@ async function createBuildManifest(
};
}

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

csp = {
clientStyleHashes,
Expand Down
16 changes: 12 additions & 4 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import type {
} from '../../app/types.js';
import {
getAlgorithm,
getScriptHashes,
getStyleHashes,
shouldTrackCspHashes,
trackScriptHashes,
trackStyleHashes,
Expand Down Expand Up @@ -283,10 +285,16 @@ async function buildManifest(

let csp = undefined;

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

csp = {
clientStyleHashes,
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,8 @@ 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(),
}),
])
.optional()
Expand Down
31 changes: 31 additions & 0 deletions packages/astro/src/core/config/schemas/refined.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,35 @@ 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) {
if (
!(hash.startsWith('sha256-') || hash.startsWith('sha384-') || hash.startsWith('sha512-'))
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `**scriptHashes** property "${hash}" must start with "sha256-", "sha384-" or "sha512-" to be valid.`,
path: ['experimental', 'csp', 'scriptHashes'],
});
}
}
}

if (styleHashes) {
for (const hash of styleHashes) {
if (
!(hash.startsWith('sha256-') || hash.startsWith('sha384-') || hash.startsWith('sha512-'))
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `**styleHashes** property "${hash}" must start with "sha256-", "sha384-" or "sha512-" to be valid.`,
path: ['experimental', 'csp', 'styleHashes'],
});
}
}
}
}
});
39 changes: 25 additions & 14 deletions packages/astro/src/core/csp/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,35 @@ import type { AstroConfig, CspAlgorithm } from '../../types/public/index.js';
import type { BuildInternals } from '../build/internal.js';
import { generateCspDigest } from '../encryption.js';

export function shouldTrackCspHashes(config: AstroConfig): boolean {
return config.experimental?.csp === true || typeof config.experimental?.csp === 'object';
export function shouldTrackCspHashes(
csp: any,
): csp is Exclude<AstroConfig['experimental']['csp'], false> {
return csp === true || typeof 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) {
export function getAlgorithm(
csp: Exclude<AstroConfig['experimental']['csp'], false>,
): CspAlgorithm {
if (csp === true) {
return 'SHA-256';
}
return config.experimental.csp.algorithm;
return csp.algorithm;
}

export function getScriptHashes(csp: Exclude<AstroConfig['experimental']['csp'], false>): string[] {
if (csp === true) {
return [];
} else {
return csp.scriptHashes ?? [];
}
}

export function getStyleHashes(csp: Exclude<AstroConfig['experimental']['csp'], false>): string[] {
if (csp === true) {
return [];
} else {
return csp.styleHashes ?? [];
}
}

export async function trackStyleHashes(
Expand Down
30 changes: 29 additions & 1 deletion packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2244,7 +2244,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
| {
/**
* @name experimental.csp.algorithm
* @type {string}
* @type {"SHA-256" | "SHA-384" | "SHA-512"}
* @default `'SHA-256'`
* @version 5.5.x
* @description
Expand All @@ -2255,6 +2255,34 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
*
*/
algorithm?: CspAlgorithm;

/**
* @name experimental.csp.styleHashes
* @type {string[]}
* @default `[]`
* @version 5.5.x
* @description
*
* A list of style hashes to include in all pages. These hashes are added to the `style-src` policy.
*
* The default value is `[]`.
*
*/
styleHashes?: `${'sha256' | 'sha512' | 'sha384'}-${string}`[];

/**
* @name experimental.csp.scriptHashes
* @type {string[]}
* @default `[]`
* @version 5.5.x
* @description
*
* A list of script hashes to include in all pages. These hashes are added to the `script-src` policy.
*
* The default value is `[]`.
*
*/
scriptHashes?: `${'sha256' | 'sha512' | 'sha384'}-${string}`[];
};

/**
Expand Down
15 changes: 10 additions & 5 deletions packages/astro/src/vite-plugin-astro-server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { fileURLToPath } from 'node:url';
import type * as vite from 'vite';
import { normalizePath } from 'vite';
import type { SSRManifestCSP, SSRManifest, SSRManifestI18n } from '../core/app/types.js';
import { getAlgorithm, shouldTrackCspHashes } from '../core/csp/common.js';
import {
getAlgorithm,
getScriptHashes,
getStyleHashes,
shouldTrackCspHashes,
} from '../core/csp/common.js';
import { warnMissingAdapter } from '../core/dev/adapter-validation.js';
import { createKey, getEnvironmentKey, hasEnvironmentKey } from '../core/encryption.js';
import { getViteErrorPayload } from '../core/errors/dev/index.js';
Expand Down Expand Up @@ -175,11 +180,11 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
};
}

if (shouldTrackCspHashes(settings.config)) {
if (shouldTrackCspHashes(settings.config.experimental.csp)) {
csp = {
clientScriptHashes: [],
clientStyleHashes: [],
algorithm: getAlgorithm(settings.config),
clientScriptHashes: getScriptHashes(settings.config.experimental.csp),
clientStyleHashes: getStyleHashes(settings.config.experimental.csp),
algorithm: getAlgorithm(settings.config.experimental.csp),
};
}

Expand Down
34 changes: 34 additions & 0 deletions packages/astro/test/csp.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,38 @@ describe('CSP', () => {
assert.fail('Should have the manifest');
}
});

it('should render hashes provided by the user', async () => {
fixture = await loadFixture({
root: './fixtures/csp/',
adapter: testAdapter({
setManifest(_manifest) {
manifest = _manifest;
},
}),
experimental: {
csp: {
styleHashes: ['sha512-hash1', 'sha384-hash2'],
scriptHashes: ['sha512-hash3', 'sha384-hash4'],
},
},
});
await fixture.build();
app = await fixture.loadTestAdapterApp();

if (manifest) {
const request = new Request('http://example.com/index.html');
const response = await app.render(request);
const html = await response.text();
const $ = cheerio.load(html);

const meta = $('meta[http-equiv="Content-Security-Policy"]');
assert.ok(meta.attr('content').toString().includes('sha384-hash2'));
assert.ok(meta.attr('content').toString().includes('sha384-hash4'));
assert.ok(meta.attr('content').toString().includes('sha512-hash1'));
assert.ok(meta.attr('content').toString().includes('sha512-hash3'));
} else {
assert.fail('Should have the manifest');
}
});
});
36 changes: 36 additions & 0 deletions packages/astro/test/units/config/config-validate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -479,4 +479,40 @@ describe('Config Validation', () => {
);
});
});

describe('csp', () => {
it('should throw an error if incorrect scriptHashes are passed', async () => {
let configError = await validateConfig({
experimental: {
csp: {
scriptHashes: ['fancy-1234567890'],
},
},
}).catch((err) => err);
assert.equal(configError instanceof z.ZodError, true);
assert.equal(
configError.errors[0].message.includes(
'**scriptHashes** property "fancy-1234567890" must start with "sha256-", "sha384-" or "sha512-" to be valid.',
),
true,
);
});

it('should throw an error if incorrect styleHashes are passed', async () => {
let configError = await validateConfig({
experimental: {
csp: {
styleHashes: ['fancy-1234567890'],
},
},
}).catch((err) => err);
assert.equal(configError instanceof z.ZodError, true);
assert.equal(
configError.errors[0].message.includes(
'**styleHashes** property "fancy-1234567890" must start with "sha256-", "sha384-" or "sha512-" to be valid.',
),
true,
);
});
});
});
Loading