-
Notifications
You must be signed in to change notification settings - Fork 33
Content Security Policy (CSP) #1168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
56d0e84
0c65955
3774bc8
6322221
c10c2c5
cc6d3bf
c3d7e5f
fd1f95f
b19ce56
1bbef1e
c8843f6
91a2e78
eb28bb3
e69e1c9
bd6fa9f
798c703
79b228e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| <!-- | ||
| Note: You are probably looking for `stage-1--discussion-template.md`! | ||
| This template is reserved for anyone championing an already-approved proposal. | ||
|
|
||
| Community members who would like to propose an idea or feature should begin | ||
| by creating a GitHub Discussion. See the repo README.md for more info. | ||
|
|
||
| To use this template: create a new, empty file in the repo under `proposals/${ID}.md`. | ||
| Replace `${ID}` with the official accepted proposal ID, found in the GitHub Issue | ||
| of the accepted proposal. | ||
| --> | ||
|
|
||
| **If you have feedback and the feature is released as experimental, please leave it on the Stage 3 PR. Otherwise, comment on the Stage 2 issue (links below).** | ||
|
|
||
| - Start Date: 2025-05-13 | ||
| - Reference Issues: | ||
| - Implementation PR: | ||
| - Stage 2 Issue: https://github.com/withastro/roadmap/issues/1149 | ||
| - Stage 3 PR: https://github.com/withastro/roadmap/discussions/377 | ||
|
|
||
| # Summary | ||
|
|
||
| Add a new feature that allows Astro assets, mainly scripts and styles, to support [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP). | ||
|
|
||
| # Example | ||
|
|
||
| ```js | ||
| // astro.config.mjs | ||
| import { defineConfig } from "astro/config"; | ||
|
|
||
| export default defineConfig({ | ||
| security: { | ||
| csp: true | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| # Background & Motivation | ||
|
|
||
| Include any useful background detail that that explains why this RFC is important. | ||
| What are the problems that this RFC sets out to solve? Why now? Be brief! | ||
|
|
||
| It can be useful to illustrate your RFC as a user problem in this section. | ||
| (ex: "Users have reported that it is difficult to do X in Astro today.") | ||
|
||
|
|
||
| # Goals | ||
|
|
||
| - Support at least one solution (`nonce`, hash, etc.) that would support CSP. | ||
| - Allow Astro scripts (client islands, server islands, view transitions, etc.) and Astro styles to work out of the box. | ||
| - Update adapters to support the feature, if needed. | ||
| - Make the feature opt-in, even when it's outside experimental. | ||
| - Support the [dev server](#dev-server) (stretch) | ||
| - View transitions (limited/opt-in support) | ||
|
|
||
| # Non-Goals | ||
|
|
||
| - Provide *multiple* solutions to fix the same problem | ||
| - Support for third-party scripts dynamically imported by users e.g. `document.createElement("script")`, `import("https://path.to/script.js")`; | ||
| - Support for third-party styles dynamically imported by users. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I undestand correctly, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to rephrase it then. What I meant is that out of the box Astro won't create a hash for the dynamically imported script/style. Any suggestions? |
||
| - Performance. Users and authors are willing to overlook possible performance drawbacks in order to increase security. | ||
|
|
||
|
|
||
| # Detailed Design | ||
|
|
||
| ## Solution adopted | ||
|
|
||
| Contrary to other frameworks of the same ecosystem, our solution will rely on generating [**hashes of styles and scripts**](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP#hashes). For more information, please refer to the [alternatives](#alternatives) section. | ||
|
|
||
| Astro provides the hashes during the rendering phase, they will be written in the `<head>` tag using the `<meta http-equiv="Content-Security-Policy">` meta tag. | ||
|
|
||
| The *main* reason for using this strategy instead of `Response` headers is because this solution allows Astro to support two important use cases: | ||
|
||
| - SSG, especially when no adapters are involved. | ||
| - SPA (Single Page Applications), where scripts and styles *might* be loaded dynamically using client-side routing. | ||
|
|
||
| ## Generation of hashes | ||
|
|
||
| The hashes will be generated using the [`crypto`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto) global object, and the package [`@oslojs/encoding`](https://encoding.oslojs.dev/). Astro *already uses* this package to encrypt props for server islands. | ||
|
|
||
| Hashes will be generated using the `SHA-256` algorithm, by default. However, users can change the algorithm via configuration. | ||
|
|
||
| ## Known scripts and styles | ||
|
|
||
| When using client islands, Astro injects **known** styles (Astro island) and scripts (Astro island and directives). These assets are known at build time, so Astro calculates the hashes at build time and stores them in the manifest. The rendering engine will just read and render them. | ||
|
|
||
| ## `ClientRouter` (AKA view transitions) | ||
|
|
||
| The client router has some complex logic to accommodate many use cases. Some of those use cases involve the swapping of scripts during the transition from one page to another. | ||
|
|
||
| The swapping of scripts is restricted in CSP, and can only be allowed using the [`strict-dynamic`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP#the_strict-dynamic_keyword) directive. | ||
|
|
||
| We might enable the client router only via configuration. | ||
|
|
||
| ```js | ||
| export default defineConfig({ | ||
| security: { | ||
| csp: { | ||
| strictDynamic: true | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| We want to expose a configuration because sometimes it's very easy to miss the usage of `ClientRouter`, while an explicit configuration will work for the `ClientRouter` **and** userland scripts. We don't want to enable a directive **implicitly** without the user's consent. | ||
|
|
||
| ## Server Islands | ||
|
|
||
| Server islands are very dynamic because the contents of their rendered scripts depend on `props` and `slots`. Due to this requirement, the server islands must support head propagation. The compiler will ensure that the correct metadata is emitted. | ||
|
|
||
| The hashes **will be generated during the rendering phase**. The performance hit is proportional to the number of server islands used in a page. | ||
|
|
||
| ## User styles | ||
|
|
||
| Compiled styles are calculated during the build, and Astro already possesses all the information on the styles imported and rendered by page. During the build, Astro will calculate the hashes of those assets and store them in the manifest. The rendering engine will just read and render them. | ||
|
|
||
| ## Rendered `<meta>` | ||
|
|
||
| The current proposal targets scripts and styles emitted by Astro, so the directives rendered will handle only `style-src` and `script-src`: | ||
| ```html | ||
| <meta | ||
| http-equiv="Content-Security-Policy" | ||
| content="style-src 'self' 'sha256-hash1'; script-src 'self' 'sha256-hash2'" | ||
| > | ||
| ``` | ||
| While browsers are able to [merge directives](https://centralcsp.com/articles/csp-meta-tags) from `Response` headers and `meta` tags, Astro will provide runtime APIs so users can push their custom policies based in their requirements. | ||
|
|
||
| ### Configuration APIs | ||
|
|
||
| New configurations will be available in case directives and/or hashes can be applied to all pages | ||
|
|
||
| ```js | ||
| export default defineConfig({ | ||
| security: { | ||
| csp: { | ||
| // or SHA-384, or SHA-256 (default) | ||
| algorithm: "SHA-512", | ||
| // Applied only to `script-src` directive | ||
| strictDynamic: true, | ||
| // Additional directives added to the final content | ||
| directives: [ | ||
| "default-src: 'self'", | ||
| "img-src 'self'" | ||
| ], | ||
| // List of hashes for `style-src`. | ||
| styleHashes: [ | ||
| "sha512-hash1", | ||
| "sha256-hash1", | ||
| "sha384-hash1", | ||
| ], | ||
| // List of hashes for `script-src`. | ||
| scriptHashes: [ | ||
| "sha512-hash1", | ||
| "sha256-hash1", | ||
| "sha384-hash1", | ||
| ], | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| ### Runtime APIs | ||
|
|
||
| The following paragraph will describe possible runtime APIs that users can use to customise their CSP policies. When `Astro.` is used, it's intended that also `APIContext` is covered by the implementation. | ||
|
|
||
| #### `Astro.insertCspDirective(directive: string)` | ||
|
|
||
| Users can add an entire directive. Astro won't check if the directive is grammatically correct. | ||
|
|
||
| ```js | ||
| Astro.insertCspPolicy(`image-src '${Astro.site}'`) | ||
|
||
| ``` | ||
| Result | ||
| ```html | ||
| <meta | ||
| http-equiv="Content-Security-Policy" | ||
| content="image-src 'https://example.com'; style-src 'self' 'sha256-hash1'; script-src 'self' 'sha256-hash2'" | ||
florian-lefebvre marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
ematipico marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| > | ||
| ``` | ||
|
|
||
| #### `Astro.insertCspAsset({ content: string, kind: "script" | "style" })` | ||
|
|
||
| ```js | ||
| Astro.insertCspAsset({ | ||
| content: "console.log(sort)", | ||
| kind: "script" | ||
| }) | ||
| ``` | ||
| Result | ||
| ```html | ||
| <meta | ||
| http-equiv="Content-Security-Policy" | ||
| content="style-src 'self' 'sha256-hash1'; script-src 'self' 'sha256-hash2' 'sha256-<NEW_HASH>'" | ||
| > | ||
| ``` | ||
| Where `<NEW_HASH>` is the hash generated by Astro from the `console.log(sort)` content. | ||
|
|
||
|
|
||
| # Testing Strategy | ||
|
|
||
| Types of tests: | ||
| - unit tests | ||
| - integration tests | ||
| - e2e tests | ||
|
|
||
| Features to test: | ||
| - client islands | ||
| - server islands | ||
| - view transitions | ||
| - external scripts (via APIs) | ||
| - external styles (via APIs) | ||
| - inline scripts | ||
| - inline styles | ||
|
|
||
| # Drawbacks | ||
|
|
||
| The hashes solution will negatively impact the rendering engine because Astro will have to calculate the hashes **at runtime**. The most expensive solution is the support of server islands because the contents of their scripts depend on props and slots, which are only known during the rendering phase. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we know how big the impact is? Genuinely curious There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, but we can set up a codspeed benchmark There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On a comparable solution, it would take around 500ms for dozens of pages in a Netlify Free tier build. This should be the base performance |
||
|
|
||
| However, users are willing to compromise some performance penalties for security. | ||
|
|
||
| ## Dev server | ||
|
|
||
| `vite` works differently in dev and prod. In dev, all assets (scripts and styles) are inlined, and also replaced using HRM. After the build, these assets are bundled, minified and imported differently. | ||
|
|
||
| Due to this significant discrepancy, supporting the dev server will be very difficult, so we will focus on the build first. | ||
|
|
||
| # Alternatives | ||
|
|
||
| ## The `nonce` solution | ||
|
|
||
| This solution is way **easier** to implement; however, it won't work for static pages (SSG). | ||
| As highlighted in this [comment of mine](https://github.com/withastro/roadmap/issues/1149#issuecomment-2824552131), in order to provide the `nonce` header, a user would need to use **an edge function** in a serverless environment or middleware for server environments. This means that environments that don't provide any virtual host control (e.g. GitHub pages, etc.), the solution wouldn't work *at all*. | ||
|
|
||
| This solution doesn't follow Astro core principles, where an Astro site should work *out of the box* with the majority of environments, especially SSG, which is a first-class citizen for us. | ||
|
|
||
|
|
||
| # Adoption strategy | ||
|
|
||
| The usage of this feature will be under an experimental flag, and shipped in a minor: | ||
|
|
||
| ```js | ||
| export default defineConfig({ | ||
| experimental: { | ||
| csp: true | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| After a period of trial and test with existing Astro functionalities, the experimental flag will be removed. | ||
|
|
||
| The feature will always be **opt-in**. | ||
|
|
||
| # Unresolved Questions | ||
Uh oh!
There was an error while loading. Please reload this page.