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
2 changes: 1 addition & 1 deletion packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ function createManifest(
clientScriptHashes: manifest?.clientScriptHashes ?? [],
clientStyleHashes: manifest?.clientStyleHashes ?? [],
shouldInjectCspMetaTags: manifest?.shouldInjectCspMetaTags ?? false,
astroIslandHashes: manifest?.astroIslandHashes ?? [],
astroIslandHashes: manifest?.astroIslandHashes ?? {},
};
}

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 @@ -92,7 +92,7 @@ export type SSRManifest = {
* When enabled, Astro tracks the hashes of script and styles, and eventually it will render the `<meta>` tag
*/
shouldInjectCspMetaTags: boolean;
astroIslandHashes: string[];
astroIslandHashes: Record<string, string>;
};

export type SSRActions = {
Expand Down
19 changes: 9 additions & 10 deletions packages/astro/src/core/astro-islands-hashes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
// This file is code-generated, please don't change it manually
export const ASTRO_ISLAND_HASHES = [
"GI/D8grziRZwfj/Mqmn+dcgU/i8sylHSR/IfobqcUT4=",
"HDWxd14AUw8OvjrhhRRyyZFHCGnzxXGDrg59Qi8ayhc=",
"XN6a2Vn8uvpBr/WhdYPdK0jVeCzlcOD2XYaP10veV4Y=",
"ZR0ZAU8UNTzLmo/ApeWH0y1mVLT+XtFkvZ5nw32W8jI=",
"cSNmhdbFlyTDRozeu9HPjo+B2S4QAeMp0RO41PqgAcA=",
"mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI=",
"mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI=",
"s81ZcLcyAa7P/Jh5M5hUxYthTGwW+iZY3e6aHrQ8H9E="
];
export const ASTRO_ISLAND_HASHES = {
"astro-island": "p9VbHs/ClkQc+x63XdUjvCAgeWxA4ZGvpebJtMn9jbs=",
"idle": "BF0290pkb3jxQsE7z00xR8Imp8X34FLC88L0lkMnrGw=",
"load": "QzWFZi+FLIx23tnm9SBU4aEgx4x8DsuASP07mfqol/c=",
"media": "0chmwFk0zaA528yFfGV7J9ppIpdfTPPULncDF3WG7Zs=",
"only": "eIXWvAmxkr251LJZkjniEK5LcPF3NkapbJepohwYRIc=",
"visible": "Q2BPg90ZMplYY+FSdApNErhpWafg2hcRRbndmvxuL/Q=",
"astro-island-styles": "s81ZcLcyAa7P/Jh5M5hUxYthTGwW+iZY3e6aHrQ8H9E="
};
12 changes: 9 additions & 3 deletions packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { NOOP_MIDDLEWARE_FN } from './middleware/noop-middleware.js';
import { sequence } from './middleware/sequence.js';
import { RouteCache } from './render/route-cache.js';
import { createDefaultRoutes } from './routing/default.js';
import { createCSPMiddleware } from './csp/middleware.js';

/**
* The `Pipeline` represents the static parts of rendering that do not change between requests.
Expand Down Expand Up @@ -112,11 +113,16 @@ export abstract class Pipeline {
else if (this.middleware) {
const middlewareInstance = await this.middleware();
const onRequest = middlewareInstance.onRequest ?? NOOP_MIDDLEWARE_FN;
const internalMiddlewares = [onRequest];
if (this.manifest.checkOrigin) {
this.resolvedMiddleware = sequence(createOriginCheckMiddleware(), onRequest);
} else {
this.resolvedMiddleware = onRequest;
// this middleware must be placed at the beginning because it needs to block incoming requests
internalMiddlewares.unshift(createOriginCheckMiddleware());
}
if (this.manifest.shouldInjectCspMetaTags) {
// this middleware must be placed at the end because it needs to inject the CSP headers
internalMiddlewares.push(createCSPMiddleware());
}
this.resolvedMiddleware = sequence(...internalMiddlewares);
return this.resolvedMiddleware;
} else {
this.resolvedMiddleware = NOOP_MIDDLEWARE_FN;
Expand Down
12 changes: 7 additions & 5 deletions packages/astro/src/core/csp/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ export function trackStyleHashes(internals: BuildInternals): string[] {
for (const [_, page] of internals.pagesByViteID.entries()) {
for (const style of page.styles) {
if (style.sheet.type === 'inline') {
clientStyleHashes.push(
crypto.createHash('sha256').update(style.sheet.content).digest('base64'),
);
clientStyleHashes.push(generateHash(style.sheet.content));
}
}
}
Expand All @@ -26,15 +24,19 @@ export function trackScriptHashes(internals: BuildInternals, settings: AstroSett
const clientScriptHashes: string[] = [];

for (const script of internals.inlinedScripts.values()) {
clientScriptHashes.push(crypto.createHash('sha256').update(script).digest('base64'));
clientScriptHashes.push(generateHash(script));
}

for (const script of settings.scripts) {
const { content, stage } = script;
if (stage === 'head-inline' || stage === 'before-hydration') {
clientScriptHashes.push(crypto.createHash('sha256').update(content).digest('base64'));
clientScriptHashes.push(generateHash(content));
}
}

return clientScriptHashes;
}

function generateHash(content: string): string {
return crypto.createHash('sha256').update(content).digest('base64');
}
11 changes: 11 additions & 0 deletions packages/astro/src/core/csp/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { MiddlewareHandler } from '../../types/public/index.js';

export function createCSPMiddleware(): MiddlewareHandler {
return async (_, next) => {
const response = await next();

// Do something with the response

return response;
};
}
1 change: 1 addition & 0 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ export class RenderContext {
shouldInjectCspMetaTags: manifest.shouldInjectCspMetaTags,
clientScriptHashes: manifest.clientScriptHashes,
clientStyleHashes: manifest.clientStyleHashes,
astroIslandHashes: manifest.astroIslandHashes,
};

return result;
Expand Down
13 changes: 5 additions & 8 deletions packages/astro/src/runtime/server/render/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { SSRResult } from '../../../types/public/internal.js';
import type { HTMLBytes, HTMLString } from '../escape.js';
import { markHTMLString } from '../escape.js';
import {
type PrescriptType,
determineIfNeedsHydrationScript,
determinesIfNeedsDirectiveScript,
getPrescripts,
Expand Down Expand Up @@ -65,13 +64,11 @@ function stringifyChunk(
let needsDirectiveScript =
hydration && determinesIfNeedsDirectiveScript(result, hydration.directive);

let prescriptType: PrescriptType = needsHydrationScript
? 'both'
: needsDirectiveScript
? 'directive'
: null;
if (prescriptType) {
let prescripts = getPrescripts(result, prescriptType, hydration.directive);
if (needsHydrationScript) {
let prescripts = getPrescripts(result, 'both', hydration.directive);
return markHTMLString(prescripts);
} else if (needsDirectiveScript) {
let prescripts = getPrescripts(result, 'directive', hydration.directive);
return markHTMLString(prescripts);
} else {
return '';
Expand Down
28 changes: 28 additions & 0 deletions packages/astro/src/runtime/server/render/csp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { SSRResult } from '../../../types/public/index.js';

export function renderCspContent(result: SSRResult): string {
const finalScriptHashes = new Set();
const finalStyleHashes = new Set();

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

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

if (result.renderers.length > 0) {
for (const [ name, hash ] of Object.entries(result.astroIslandHashes)) {
if (name === 'astro-island-styles') {
finalStyleHashes.add(`'sha256-${hash}'`);
} else {
finalScriptHashes.add(`'sha256-${hash}'`);
}
}
}

const scriptSrc = `style-src 'self' ${Array.from(finalStyleHashes).join(' ')};`;
const styleSrc = `script-src 'self' ${Array.from(finalScriptHashes).join(' ')};`;
return `${scriptSrc} ${styleSrc}`;
}
30 changes: 12 additions & 18 deletions packages/astro/src/runtime/server/render/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { markHTMLString } from '../escape.js';
import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './instruction.js';
import { createRenderInstruction } from './instruction.js';
import { renderElement } from './util.js';
import { renderCspContent } from './csp.js';

// Filter out duplicate elements in our set
const uniqueElements = (item: any, index: number, all: any[]) => {
Expand Down Expand Up @@ -51,26 +52,19 @@ export function renderAllHeadContent(result: SSRResult) {
}
}

const hashes = [];

if (result.shouldInjectCspMetaTags) {
for (const scriptHash of [...result.clientScriptHashes, ...result.clientStyleHashes]) {
hashes.push(
renderElement(
'meta',
{
props: {
'http-equiv': 'content-security-policy',
content: scriptHash,
},
children: '',
},
false,
),
);
}
content += renderElement(
'meta',
{
props: {
'http-equiv': 'content-security-policy',
content: renderCspContent(result),
},
children: '',
},
false,
);
}
content += hashes.join('\n');

return markHTMLString(content);
}
Expand Down
9 changes: 3 additions & 6 deletions packages/astro/src/runtime/server/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function determinesIfNeedsDirectiveScript(result: SSRResult, directive: s
return true;
}

export type PrescriptType = null | 'both' | 'directive';
export type PrescriptType = 'both' | 'directive';

function getDirectiveScriptText(result: SSRResult, directive: string): string {
const clientDirectives = result.clientDirectives;
Expand All @@ -31,8 +31,8 @@ function getDirectiveScriptText(result: SSRResult, directive: string): string {

export function getPrescripts(result: SSRResult, type: PrescriptType, directive: string): string {
// Note that this is a classic script, not a module script.
// This is so that it executes immediate, and when the browser encounters
// an astro-island element the callbacks will fire immediately, causing the JS
// This is so that it executes immediately, and when the browser encounters
// an astro-island element, the callbacks will fire immediately, causing the JS
// deps to be loaded immediately.
switch (type) {
case 'both':
Expand All @@ -41,8 +41,5 @@ export function getPrescripts(result: SSRResult, type: PrescriptType, directive:
}</script>`;
case 'directive':
return `<script>${getDirectiveScriptText(result, directive)}</script>`;
case null:
break;
}
return '';
}
5 changes: 2 additions & 3 deletions packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2229,12 +2229,11 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
*/
headingIdCompat?: boolean;


/**
*
*
*/
// TODO: add docs once we are reaching the end
csp?: boolean,
csp?: boolean;

/**
* @name experimental.preserveScriptOrder
Expand Down
6 changes: 4 additions & 2 deletions packages/astro/src/types/public/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Params } from './common.js';
import type { AstroConfig, RedirectConfig } from './config.js';
import type { AstroGlobal, AstroGlobalPartial } from './context.js';
import type { AstroRenderer } from './integrations.js';
import type { SSRManifest } from '../../core/app/types.js';

export type { SSRManifest } from '../../core/app/types.js';

Expand Down Expand Up @@ -250,8 +251,9 @@ export interface SSRResult {
* Whether Astro should inject the CSP <meta> tag into the head of the component.
*/
shouldInjectCspMetaTags: boolean;
clientScriptHashes: string[];
clientStyleHashes: string[];
clientScriptHashes: SSRManifest['clientScriptHashes'];
clientStyleHashes: SSRManifest['clientStyleHashes'];
astroIslandHashes: SSRManifest['astroIslandHashes'];
}

/**
Expand Down
26 changes: 24 additions & 2 deletions packages/astro/test/csp.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,32 @@ describe('CSP', () => {
const response = await app.render(request);
const $ = cheerio.load(await response.text());

const meta = $('meta[http-equiv="Content-Security-Policy"]');
for (const hash of manifest.clientStyleHashes) {
let meta = $('meta[http-equiv="Content-Security-Policy"][content="' + hash + '"]');
assert.equal(meta.length, 1, `Should have a CSP meta tag for ${hash}`);
assert.match(
meta.attr('content'),
new RegExp(`'sha256-${hash}'`),
`Should have a CSP meta tag for ${hash}`,
);
}

let [, astroStyleHash] = Object.entries(manifest.astroIslandHashes).find(
([name, _]) => name === 'astro-island-styles',
);
astroStyleHash = `sha256-${astroStyleHash}`;

let [, astroIsland] = Object.entries(manifest.astroIslandHashes).find(([name, _]) => name === 'astro-island');
astroIsland = `sha256-${astroIsland}`;

assert.ok(
meta.attr('content').includes(astroStyleHash),
`Should have a CSP meta tag for ${astroStyleHash}`,
);

assert.ok(
meta.attr('content').includes(astroIsland),
`Should have a CSP meta tag for ${astroIsland}`,
);
} else {
assert.fail('Should have the manifest');
}
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/test/fixtures/csp/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';

export default defineConfig({
experimental: {
csp: true,
}
},
integrations: [
react()
],
});

5 changes: 4 additions & 1 deletion packages/astro/test/fixtures/csp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
"astro": "workspace:*",
"@astrojs/react": "workspace:*",
"react": "^19.1.0",
"react-dom": "^19.1.0"
}
}
5 changes: 5 additions & 0 deletions packages/astro/test/fixtures/csp/src/components/Text.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@


export function Text() {
return "Text"
}
18 changes: 18 additions & 0 deletions packages/astro/test/fixtures/csp/src/pages/react.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
import {Text} from "../components/Text.jsx"
---


<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width"/>
<title>Index</title>
</head>
<body>
<main>
<h1>React</h1>
<Text client:load />
</main>
</body>
</html>
Loading