Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 10 additions & 2 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 @@ -632,8 +634,14 @@ 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);
const clientScriptHashes = [
...getScriptHashes(settings.config),
...(await trackScriptHashes(internals, settings, algorithm)),
];
const clientStyleHashes = [
...getStyleHashes(settings.config),
...(await trackStyleHashes(internals, settings, algorithm)),
];

csp = {
clientStyleHashes,
Expand Down
12 changes: 10 additions & 2 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 @@ -285,8 +287,14 @@ async function buildManifest(

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);
const clientScriptHashes = [
...getScriptHashes(settings.config),
...(await trackScriptHashes(internals, settings, algorithm)),
];
const clientStyleHashes = [
...getStyleHashes(settings.config),
...(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'],
});
}
}
}
}
});
30 changes: 30 additions & 0 deletions packages/astro/src/core/csp/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,36 @@ export function getAlgorithm(config: AstroConfig): CspAlgorithm {
return config.experimental.csp.algorithm;
}

/**
* Use this function when after you checked that CSP is enabled, or it throws an error.
* @param config
*/
export function getScriptHashes(config: AstroConfig): string[] {
if (!config.experimental?.csp) {
throw new Error('CSP is not enabled');
}
if (config.experimental?.csp === true) {
return [];
} else {
return config.experimental.csp.scriptHashes ?? [];
}
}

/**
* Use this function when after you checked that CSP is enabled, or it throws an error.
* @param config
*/
export function getStyleHashes(config: AstroConfig): string[] {
if (!config.experimental?.csp) {
throw new Error('CSP is not enabled');
}
if (config.experimental?.csp === true) {
return [];
} else {
return config.experimental.csp.styleHashes ?? [];
}
}

export async function trackStyleHashes(
internals: BuildInternals,
settings: AstroSettings,
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?: 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?: string[];
};

/**
Expand Down
11 changes: 8 additions & 3 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 @@ -177,8 +182,8 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest

if (shouldTrackCspHashes(settings.config)) {
csp = {
clientScriptHashes: [],
clientStyleHashes: [],
clientScriptHashes: getScriptHashes(settings.config),
Copy link
Member

Choose a reason for hiding this comment

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

Semi related to this PR: why aren't you calling trackStyleHashes/trackScriptHashes?

Copy link
Member Author

@ematipico ematipico May 15, 2025

Choose a reason for hiding this comment

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

They are meant to be called during the build. They accept BuildInternals, which is a type that we populate during the build. The development server doesn't have such things.

clientStyleHashes: getStyleHashes(settings.config),
algorithm: getAlgorithm(settings.config),
};
}
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,
);
});
});
});