Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
28 changes: 28 additions & 0 deletions .changeset/crazy-doors-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'astro': minor
---

Adds experimental Content Security Policy (CSP) support

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.

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.

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.

To enable this feature, add the experimental flag in your Astro config:

```js
// astro.config.mjs
import { defineConfig } from "astro/config"

export default defineConfig({
experimental: {
csp: true
}
})
```

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/).

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).
126 changes: 126 additions & 0 deletions packages/astro/e2e/csp-client-only.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { expect } from '@playwright/test';
import { testFactory } from './test-utils.js';

const test = testFactory(import.meta.url, {
root: './fixtures/client-only/',
experimental: {
csp: true,
},
});

let previewServer;

test.beforeAll(async ({ astro }) => {
await astro.build();
previewServer = await astro.preview();
});

test.afterAll(async () => {
await previewServer.stop();
});
test.describe('CSP Client only', () => {
test('React counter', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

const counter = await page.locator('#react-counter');
await expect(counter, 'component is visible').toBeVisible();

const fallback = await page.locator('[data-fallback=react]');
await expect(fallback, 'fallback content is hidden').not.toBeVisible();

const count = await counter.locator('pre');
await expect(count, 'initial count is 0').toHaveText('0');

const children = await counter.locator('.children');
await expect(children, 'children exist').toHaveText('react');

const increment = await counter.locator('.increment');
await increment.click();

await expect(count, 'count incremented by 1').toHaveText('1');
});

test('Preact counter', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

const counter = await page.locator('#preact-counter');
await expect(counter, 'component is visible').toBeVisible();

const fallback = await page.locator('[data-fallback=preact]');
await expect(fallback, 'fallback content is hidden').not.toBeVisible();

const count = await counter.locator('pre');
await expect(count, 'initial count is 0').toHaveText('0');

const children = await counter.locator('.children');
await expect(children, 'children exist').toHaveText('preact');

const increment = await counter.locator('.increment');
await increment.click();

await expect(count, 'count incremented by 1').toHaveText('1');
});

test('Solid counter', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

const counter = await page.locator('#solid-counter');
await expect(counter, 'component is visible').toBeVisible();

const fallback = await page.locator('[data-fallback=solid]');
await expect(fallback, 'fallback content is hidden').not.toBeVisible();

const count = await counter.locator('pre');
await expect(count, 'initial count is 0').toHaveText('0');

const children = await counter.locator('.children');
await expect(children, 'children exist').toHaveText('solid');

const increment = await counter.locator('.increment');
await increment.click();

await expect(count, 'count incremented by 1').toHaveText('1');
});

test('Vue counter', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

const counter = await page.locator('#vue-counter');
await expect(counter, 'component is visible').toBeVisible();

const fallback = await page.locator('[data-fallback=vue]');
await expect(fallback, 'fallback content is hidden').not.toBeVisible();

const count = await counter.locator('pre');
await expect(count, 'initial count is 0').toHaveText('0');

const children = await counter.locator('.children');
await expect(children, 'children exist').toHaveText('vue');

const increment = await counter.locator('.increment');
await increment.click();

await expect(count, 'count incremented by 1').toHaveText('1');
});

test('Svelte counter', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

const counter = await page.locator('#svelte-counter');
await expect(counter, 'component is visible').toBeVisible();

const fallback = await page.locator('[data-fallback=svelte]');
await expect(fallback, 'fallback content is hidden').not.toBeVisible();

const count = await counter.locator('pre');
await expect(count, 'initial count is 0').toHaveText('0');

const children = await counter.locator('.children');
await expect(children, 'children exist').toHaveText('svelte');

const increment = await counter.locator('.increment');
await increment.click();

await expect(count, 'count incremented by 1').toHaveText('1');
});
});
39 changes: 39 additions & 0 deletions packages/astro/e2e/csp-server-islands.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expect } from '@playwright/test';
import { testFactory } from './test-utils.js';

const test = testFactory(import.meta.url, {
root: './fixtures/csp-server-islands/',
});

test.describe('CSP Server islands', () => {
test.describe('Production', () => {
let previewServer;

test.beforeAll(async ({ astro }) => {
// Playwright's Node version doesn't have these functions, so stub them.
process.stdout.clearLine = () => {};
process.stdout.cursorTo = () => {};
await astro.build();
previewServer = await astro.preview();
});

test.afterAll(async () => {
await previewServer.stop();
});

test('Only one component in prod', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/base/'));

let el = page.locator('#basics .island');

await expect(el, 'element rendered').toBeVisible();
await expect(el, 'should have content').toHaveText('I am an island');
});

test('Props are encrypted', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
let el = page.locator('#basics .secret');
await expect(el).toHaveText('test');
});
});
});
Loading