Skip to content

Commit 0eafe14

Browse files
ematipicoflorian-lefebvreascorbicsarah11918ArmandPhilippot
authored
feat: experimental CSP (#13802)
* chore: build hashes of scripts (#13590) * chore: build hashes of scripts * chore: fix changes * chore: fix changes * chore: fix changes * feat(csp): create hashes of tracked scripts and hashes (#13675) Co-authored-by: florian-lefebvre <[email protected]> * feat(csp): fix CSP header, inject astro island script/style (#13687) * feat(csp): track client scripts and CSS (#13725) Co-authored-by: ascorbic <[email protected]> * feat(csp): support view transitions (#13738) Co-authored-by: florian-lefebvre <[email protected]> Co-authored-by: ascorbic <[email protected]> fix CSP header, inject astro island script/style (#13687) * feat(csp): server islands (#13775) Co-authored-by: florian-lefebvre <[email protected]> * feat(csp): customise algorithm (#13803) Co-authored-by: Florian Lefebvre <[email protected]> * chore: build hashes of scripts (#13590) (#13805) Co-authored-by: Florian Lefebvre <[email protected]> * feat(csp): allow additional directives (#13810) Co-authored-by: ascorbic <[email protected]> Co-authored-by: florian-lefebvre <[email protected]> * feat(csp): resources for script and styles directives (#13812) Co-authored-by: ascorbic <[email protected]> * feat(csp): runtime APIs (#13824) Co-authored-by: Matt Kane <[email protected]> * feat(csp): add script-dynamic keyword support (#13834) * update lockfile * chore: docs and changeset (#13870) * chore: add changeset * grammar * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <[email protected]> * Update JSDoc with examples to match docs * Sarah's changeset edits * Apply suggestions from code review Thanks, @ArmandPhilippot Co-authored-by: Armand Philippot <[email protected]> * Fix indentation * Update .changeset/crazy-doors-buy.md * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <[email protected]> --------- Co-authored-by: Sarah Rainsberger <[email protected]> Co-authored-by: Matt Kane <[email protected]> Co-authored-by: Armand Philippot <[email protected]> * Update lockfile * dedupe deps * Lock * Lock * fix: server islands in mdx --------- Co-authored-by: florian-lefebvre <[email protected]> Co-authored-by: ascorbic <[email protected]> Co-authored-by: Florian Lefebvre <[email protected]> Co-authored-by: Matt Kane <[email protected]> Co-authored-by: Sarah Rainsberger <[email protected]> Co-authored-by: Armand Philippot <[email protected]>
1 parent 3558ca6 commit 0eafe14

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+3749
-207
lines changed

.changeset/crazy-doors-buy.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'astro': minor
3+
---
4+
5+
Adds experimental Content Security Policy (CSP) support
6+
7+
CSP is an important feature to provide fine-grained control over resources that can or cannot be downloaded and executed by a document. In particular, it can help protect against [cross-site scripting (XSS)](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting) attacks.
8+
9+
Enabling this feature adds additional security to Astro's handling of processed and bundled scripts and styles by default, and allows you to further configure these, and additional, content types. This new experimental feature has been designed to work in every Astro rendering environment (static pages, dynamic pages and single page applications), while giving you maximum flexibility and with type-safety in mind.
10+
11+
It is compatible with most of Astro's features such as client islands, and server islands, although Astro's view transitions using the `<ClientRouter />` are not yet fully supported. Inline scripts are not supported out of the box, but you can provide your own hashes for external and inline scripts.
12+
13+
To enable this feature, add the experimental flag in your Astro config:
14+
15+
```js
16+
// astro.config.mjs
17+
import { defineConfig } from "astro/config"
18+
19+
export default defineConfig({
20+
experimental: {
21+
csp: true
22+
}
23+
})
24+
```
25+
26+
For more information on enabling and using this feature in your project, see the [Experimental CSP docs](https://docs.astro.build/en/reference/experimental-flags/csp/).
27+
28+
For a complete overview, and to give feedback on this experimental API, see the [Content Security Policy RFC](https://github.com/withastro/roadmap/blob/feat/rfc-csp/proposals/0055-csp.md).
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { expect } from '@playwright/test';
2+
import { testFactory } from './test-utils.js';
3+
4+
const test = testFactory(import.meta.url, {
5+
root: './fixtures/client-only/',
6+
experimental: {
7+
csp: true,
8+
},
9+
});
10+
11+
let previewServer;
12+
13+
test.beforeAll(async ({ astro }) => {
14+
await astro.build();
15+
previewServer = await astro.preview();
16+
});
17+
18+
test.afterAll(async () => {
19+
await previewServer.stop();
20+
});
21+
test.describe('CSP Client only', () => {
22+
test('React counter', async ({ astro, page }) => {
23+
await page.goto(astro.resolveUrl('/'));
24+
25+
const counter = await page.locator('#react-counter');
26+
await expect(counter, 'component is visible').toBeVisible();
27+
28+
const fallback = await page.locator('[data-fallback=react]');
29+
await expect(fallback, 'fallback content is hidden').not.toBeVisible();
30+
31+
const count = await counter.locator('pre');
32+
await expect(count, 'initial count is 0').toHaveText('0');
33+
34+
const children = await counter.locator('.children');
35+
await expect(children, 'children exist').toHaveText('react');
36+
37+
const increment = await counter.locator('.increment');
38+
await increment.click();
39+
40+
await expect(count, 'count incremented by 1').toHaveText('1');
41+
});
42+
43+
test('Preact counter', async ({ astro, page }) => {
44+
await page.goto(astro.resolveUrl('/'));
45+
46+
const counter = await page.locator('#preact-counter');
47+
await expect(counter, 'component is visible').toBeVisible();
48+
49+
const fallback = await page.locator('[data-fallback=preact]');
50+
await expect(fallback, 'fallback content is hidden').not.toBeVisible();
51+
52+
const count = await counter.locator('pre');
53+
await expect(count, 'initial count is 0').toHaveText('0');
54+
55+
const children = await counter.locator('.children');
56+
await expect(children, 'children exist').toHaveText('preact');
57+
58+
const increment = await counter.locator('.increment');
59+
await increment.click();
60+
61+
await expect(count, 'count incremented by 1').toHaveText('1');
62+
});
63+
64+
test('Solid counter', async ({ astro, page }) => {
65+
await page.goto(astro.resolveUrl('/'));
66+
67+
const counter = await page.locator('#solid-counter');
68+
await expect(counter, 'component is visible').toBeVisible();
69+
70+
const fallback = await page.locator('[data-fallback=solid]');
71+
await expect(fallback, 'fallback content is hidden').not.toBeVisible();
72+
73+
const count = await counter.locator('pre');
74+
await expect(count, 'initial count is 0').toHaveText('0');
75+
76+
const children = await counter.locator('.children');
77+
await expect(children, 'children exist').toHaveText('solid');
78+
79+
const increment = await counter.locator('.increment');
80+
await increment.click();
81+
82+
await expect(count, 'count incremented by 1').toHaveText('1');
83+
});
84+
85+
test('Vue counter', async ({ astro, page }) => {
86+
await page.goto(astro.resolveUrl('/'));
87+
88+
const counter = await page.locator('#vue-counter');
89+
await expect(counter, 'component is visible').toBeVisible();
90+
91+
const fallback = await page.locator('[data-fallback=vue]');
92+
await expect(fallback, 'fallback content is hidden').not.toBeVisible();
93+
94+
const count = await counter.locator('pre');
95+
await expect(count, 'initial count is 0').toHaveText('0');
96+
97+
const children = await counter.locator('.children');
98+
await expect(children, 'children exist').toHaveText('vue');
99+
100+
const increment = await counter.locator('.increment');
101+
await increment.click();
102+
103+
await expect(count, 'count incremented by 1').toHaveText('1');
104+
});
105+
106+
test('Svelte counter', async ({ astro, page }) => {
107+
await page.goto(astro.resolveUrl('/'));
108+
109+
const counter = await page.locator('#svelte-counter');
110+
await expect(counter, 'component is visible').toBeVisible();
111+
112+
const fallback = await page.locator('[data-fallback=svelte]');
113+
await expect(fallback, 'fallback content is hidden').not.toBeVisible();
114+
115+
const count = await counter.locator('pre');
116+
await expect(count, 'initial count is 0').toHaveText('0');
117+
118+
const children = await counter.locator('.children');
119+
await expect(children, 'children exist').toHaveText('svelte');
120+
121+
const increment = await counter.locator('.increment');
122+
await increment.click();
123+
124+
await expect(count, 'count incremented by 1').toHaveText('1');
125+
});
126+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { expect } from '@playwright/test';
2+
import { testFactory } from './test-utils.js';
3+
4+
const test = testFactory(import.meta.url, {
5+
root: './fixtures/csp-server-islands/',
6+
});
7+
8+
test.describe('CSP Server islands', () => {
9+
test.describe('Production', () => {
10+
let previewServer;
11+
12+
test.beforeAll(async ({ astro }) => {
13+
// Playwright's Node version doesn't have these functions, so stub them.
14+
process.stdout.clearLine = () => {};
15+
process.stdout.cursorTo = () => {};
16+
await astro.build();
17+
previewServer = await astro.preview();
18+
});
19+
20+
test.afterAll(async () => {
21+
await previewServer.stop();
22+
});
23+
24+
test('Only one component in prod', async ({ page, astro }) => {
25+
await page.goto(astro.resolveUrl('/base/'));
26+
27+
let el = page.locator('#basics .island');
28+
29+
await expect(el, 'element rendered').toBeVisible();
30+
await expect(el, 'should have content').toHaveText('I am an island');
31+
});
32+
33+
test('Props are encrypted', async ({ page, astro }) => {
34+
await page.goto(astro.resolveUrl('/'));
35+
let el = page.locator('#basics .secret');
36+
await expect(el).toHaveText('test');
37+
});
38+
});
39+
});

0 commit comments

Comments
 (0)