Skip to content
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

feat: support for CSP headers on SSR mode #43

Merged
merged 5 commits into from
Mar 26, 2024
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
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,45 @@ export default defineConfig({
// parameter if you don't need this data, but it can be useful to
// configure your CSP policies.
sriHashesModule: resolve(rootDir, 'src', 'utils', 'sriHashes.mjs'),

// - If set, it controls how the security headers will be generated in the
// middleware.
// - If not set, no security headers will be generated in the middleware.
securityHeaders: {
// For now, we can only control CSP headers, but we'll add more options
// in the future.
// - If set, it controls how the CSP (Content Security Policy) header will be
// generated in the middleware.
// - If not set, no CSP header will be generated in the middleware.
contentSecurityPolicy: {
// - If set, it controls the "default" CSP directives (they can be overriden
// at runtime).
// - If not set, the middleware will use a minimal set of default directives.
cspDirectives: {
'default-src': "'none'",
}
}
}
})
]
})
```

### Generating Content-Security-Policy Headers

Although `@kindspells/astro-shield` does not generate CSP headers for you (yet),
it will make it much easier.
You can enable automated CSP headers generation by setting the option
`securityHeaders.contentSecurityPolicy` (it can be an empty object if you don't
need to customise any specific behavior, but it must be defined).

Besides enabling CSP, you can also configure its directives to some extent, via
the `cspDirectives` option.

> [!INFO]
> It is advisable to set the option `sriHashesModule` in case your dynamic pages
> include static JS or CSS resources (also: do not explicitly disable the
> `enableStatic_SRI` option if you want support for those static assets).

### Accessing metadata generated at build time

Once you run `astro build`, `@kindspells/astro-shield` will analyse the static
output and generate a new module that exports the SRI hashes, so you can use
Expand Down
77 changes: 77 additions & 0 deletions e2e/e2e.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ describe('middleware', () => {
it('patches inline resources for dynamically generated pages', async () => {
await checkHtmlIsPatched('/')
})

it('does not send csp headers when the feature is disabled', async () => {
const response = await fetch(`${baseUrl}/`)
expect(response.headers.has('content-security-policy')).toBe(false)
})
})

describe('middleware (hybrid)', () => {
Expand Down Expand Up @@ -384,6 +389,11 @@ describe('middleware (hybrid)', () => {
'/code.js': 'sha256-X7QGGDHgf6XMoabXvV9pW7gl3ALyZhZlgKq1s3pwmME=',
})
})

it('does not send csp headers when the feature is disabled', async () => {
const response = await fetch(`${baseUrl}/`)
expect(response.headers.has('content-security-policy')).toBe(false)
})
})

describe('middleware (hybrid 2)', () => {
Expand Down Expand Up @@ -453,4 +463,71 @@ describe('middleware (hybrid 2)', () => {
'/code.js': 'sha256-X7QGGDHgf6XMoabXvV9pW7gl3ALyZhZlgKq1s3pwmME=',
})
})

it('does not send csp headers when the feature is disabled', async () => {
const response = await fetch(`${baseUrl}/`)
expect(response.headers.has('content-security-policy')).toBe(false)
})
})

describe('middleware (hybrid 3)', () => {
const hybridDir = resolve(fixturesDir, 'hybrid3')
const execOpts = { cwd: hybridDir }

let baseUrl: string
let server: PreviewServer | undefined
let port: number

beforeAll(async () => {
await execFile('pnpm', ['install'], execOpts)
await execFile('pnpm', ['run', 'clean'], execOpts)
const { stdout: buildStdout } = await execFile(
'pnpm',
['run', 'build'],
execOpts,
)
expect(buildStdout).toMatch(/run the build step again/)
const { stdout: buildStdout2 } = await execFile(
'pnpm',
['run', 'build'],
execOpts,
)
expect(buildStdout2).not.toMatch(/run the build step again/)
})

beforeEach(async () => {
port = 9999 + Math.floor(Math.random() * 55536)
baseUrl = `http://localhost:${port}`

await cleanServer()
server = await preview({
root: hybridDir,
server: { port },
logLevel: 'debug',
})
})

const cleanServer = async () => {
if (server) {
if (!server.closed()) {
await server.stop()
}
server = undefined
}
}

afterEach(cleanServer)
afterAll(cleanServer) // Just in case

it('sends csp headers when the feature is enabled', async () => {
const response = await fetch(`${baseUrl}/`)
const cspHeader = response.headers.get('content-security-policy')

assert(cspHeader !== null)
assert(cspHeader)

expect(cspHeader).toBe(
"default-src 'none'; frame-ancestors 'none'; script-src 'self' 'sha256-X7QGGDHgf6XMoabXvV9pW7gl3ALyZhZlgKq1s3pwmME='; style-src 'self' 'sha256-9U7mv8FibD/D9IbGpXc86pz37l6/w4PCLpFIZuPrzh8=' 'sha256-ZlgyI5Bx/aeAyk/wSIypqeIM5PBhz9IiAek9HIiAjaI='",
)
})
})
2 changes: 1 addition & 1 deletion e2e/fixtures/dynamic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"license": "MIT",
"dependencies": {
"@astrojs/node": "^8.2.5",
"astro": "^4.5.7"
"astro": "^4.5.9"
},
"devDependencies": {
"@kindspells/astro-shield": "link:../../.."
Expand Down
Loading
Loading