Skip to content

Commit

Permalink
Merge pull request #441 from Baroshem/feat/ssg-headers
Browse files Browse the repository at this point in the history
feat(core): Security Headers for Pre-rendered Routes
  • Loading branch information
Baroshem committed May 10, 2024
2 parents 8305b48 + fdfbc9f commit 47ed844
Show file tree
Hide file tree
Showing 48 changed files with 2,678 additions and 1,531 deletions.
Expand Up @@ -117,7 +117,9 @@ security: {
ssg: {
meta: true,
hashScripts: true,
hashStyles: false
hashStyles: false,
nitroHeaders: true,
exportToPresets: true,
},
sri: true
}
Expand Down
96 changes: 83 additions & 13 deletions docs/content/1.documentation/1.getting-started/3.usage.md
Expand Up @@ -102,6 +102,12 @@ export default defineNitroPlugin((nitroApp) => {
})
```

::alert{type="warning"}
Runtime-hook configuration only applies to headers delivered on HTML pages.
<br>
Headers delivered on other resources (e.g. images, js and css files, api routes etc.) are not modifiable via runtime hooks.
::

## Configuration priority order

Nuxt-Security applies your rules in the following prority order:
Expand Down Expand Up @@ -256,31 +262,95 @@ export default defineNuxtConfig({
```


## Overwriting or modifying existing values
## Modifying security options

Within your runtime hooks, you can either overwrite or modify the existing values for any security option.
One of the easiest way to merge existing rules with your own is to use `defu`:
Within your runtime hooks, you can either modify or overwrite the existing values for any security option.

### Merging with replacement

One of the easiest way to merge existing rules with your own is to use `defuReplaceArray`:

```ts{}[server/plugins/filename.ts]
import defu from 'defu'
// You don't need to import defuReplaceArray as it is auto-imported by Nuxt Security
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-security:routeRules', async(routeRules) => {
// You can fetch configuration data asynchronously from an external source
const validDomain = await $fetch('https://some-site.com/rules')
// You can then override the security options of any route
routeRules['/some/route'] = defuReplaceArray(
{
headers: {
contentSecurityPolicy: {
"script-src": ["'self'", "..."]
// The script-src directive will be replaced with "'self' ..."
}
}
},
routeRules['/some/route'] // The other existing rules for /some/route will be preserved
)
})
})
```

In the example above,
- All existing security options for `/some/route` will be maintained, and only the `script-src` CSP directive will be modified.
- The existing content of the `script-src` directive will be erased and replaced by your values

Read more about [`defuReplaceArray`](/documentation/advanced/auto-imports/#defuReplaceArray)

::alert{type="info"}
`defuReplaceArray` is auto-imported by Nuxt Security. You can use this utility anywhere in your /server folder.
::

### Merging with addition

If you want to add additional values to the existing settings, you can use the standard `defu` utility to merge your rules.

```ts{}[server/plugins/filename.ts]
// You will need to import defu
import { defu } from 'defu'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-security:routeRules', async(routeRules) => {
routeRules['/some/route'] = defu(
{
headers: {
contentSecurityPolicy: {
"connect-src": ["'self'", validDomain]
},
xFrameOptions: false
},
hidePoweredBy: false
"script-src": ["'self'", "..."]
// The values "'self' ..." will be added to the existing values
}
}
},
routeRules['/some/route']
routeRules['/some/route'] // The other existing rules for /some/route will be preserved
)
})
})
```

In the example above,
- All existing security options for `/some/route` will be maintained, and only the `script-src` CSP directive will be modified.
- The existing content of the `script-src` directive will be preserved, and your values will be added to the existing values.

Read more about [`defu`](https://github.com/unjs/defu)


### Overwriting rules

If you want to erase the existing settings, don't use defu and overwrite the values:

```ts{}[server/plugins/filename.ts]
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-security:routeRules', async(routeRules) => {
routeRules['/some/route'] = {
headers: {
contentSecurityPolicy: {
"script-src": ["'self'", "..."]
}
}
}
// Any existing rules for /some/route will be erased
})
})
```

In the example above,
- All existing security options for `/some/route` will be erased.
- The `script-src` directive will contain your values.

1 change: 1 addition & 0 deletions docs/content/1.documentation/2.headers/1.csp.md
Expand Up @@ -255,6 +255,7 @@ export default defineNuxtConfig({
meta: true, // Enables CSP as a meta tag in SSG mode
hashScripts: true, // Enables CSP hash support for scripts in SSG mode
hashStyles: false // Disables CSP hash support for styles in SSG mode (recommended)
exportToPresets: true // Export security headers to Nitro presets
},
sri: true,
headers: {
Expand Down
30 changes: 29 additions & 1 deletion docs/content/1.documentation/5.advanced/3.strict-csp.md
Expand Up @@ -669,16 +669,42 @@ Nuxt Security uses a different approach, depending on whether SSR or SSG is used
**CSP Headers for SSG via Nitro Presets**
Nuxt Security supports CSP via HTTP headers for Nitro Presets that generate HTTP headers.
When using the SSG mode, some static hosting services such as Vercel or Netlify provide the ability to specify a configuration file that governs the value of the headers that will be generated. When these hosting services benefit from a [Nitro Preset](https://nitro.unjs.io/deploy/#overview), it is possible for Nuxt Security to predict the value of the CSP headers for each page and write the value to the configuration file.
Nuxt Security supports CSP via HTTP headers for Nitro Presets that output HTTP headers.
This feature is enabled by default with the `ssg: exportToPresets` option.
::alert{type="info"}
If you deploy your SSG site on Vercel or Netlify, you will benefit automatically from CSP Headers.
<br>
CSP will be delivered via HTTP headers, in addition to the standard `<meta http-equiv>` approach. If you want to disable the meta tag, so that only the HTTP headers are used, you can do so with the `ssg: meta` option.
::
**CSP Headers for SSG via `prerenderedHeaders` hook**
Nuxt Security allows you to generate your own headers rules with the `nuxt-security:prerenderedHeaders` buildtime hook.
If you do not deploy with a Nitro preset, or if you have specific requirements that are not met by the `ssg: exportToPresets` default, you can use this hook to generate your headers configuration file yourself.
See our documentation on the [prerenderedPages hook](/documentation/advanced/hooks/#prerendered-headers-hook)
::alert{type="info"}
This will allow you to deliver CSP via HTTP headers, in addition to the standard `<meta http-equiv>` approach.
::
**CSP Headers for Hybrid Pre-Rendered Pages**
Nuxt Security supports CSP via HTTP headers for pre-rendered pages of Hybrid applications.
This feature is enabled by default with the `ssg: nitroHeaders` option.
::alert{type="info"}
In Hybrid applications, CSP of pre-rendered pages will be delivered via HTTP headers, in addition to the standard `<meta http-equiv>` approach.
<br>
If you want to disable the meta tag, so that only the HTTP headers are used, you can do so with the `ssg: meta` option.
::
### Per Route CSP
Nuxt Security gives you the ability to define per-route CSP. For instance, you can have Strict CSP on the admin section of your application, and a more relaxed policy on the blog section.
Expand Down Expand Up @@ -718,6 +744,8 @@ export default defineNuxtConfig({
meta: true, // Enables CSP as a meta tag in SSG mode
hashScripts: true, // Enables CSP hash support for scripts in SSG mode
hashStyles: false // Disables CSP hash support for styles in SSG mode (recommended)
nitroHeaders: true // Allow Nitro to serve security headers for pre-rendered routes
exportToPresets: true // Export pre-rendered security headers to Nitro presets
},
// You can use nonce and ssg simultaneously
// Nuxt Security will take care of choosing the adequate parameters when you build for either SSR or SSG
Expand Down
190 changes: 190 additions & 0 deletions docs/content/1.documentation/5.advanced/4.hooks.md
@@ -0,0 +1,190 @@
# Nuxt Security Hooks

Use hooks to further customize Nuxt Security

---

:ellipsis{right=0px width=75% blur=150px}

## Available hooks

Nuxt Security provides two custom hooks:

1. **The routeRules hook** (Nitro runtime hook): to modify the settings of Nuxt-Security at runtime.
2. **The prerenderedHeaders hook** (Nuxt buildtime hook): to create a headers configuration file for your static server.

## Route Rules Hook

The `nuxt-security:routeRules` hook is convenient when you don't know the applicable security options at build time.

This happens most frequently in the following cases:
- Your Nuxt application is designed to be deployed on multiple websites, with a different configuration for each website.
- Your security options are kept in a third-party vault system (e.g. Google Secret Manager), and your build system does not have access to the secrets.

::alert{type="info"}
Your `nuxt-security:routeRules` hook will be invoked each time your server is restarted.
::

### How to use

In order to use this hook, you will need to write a Nitro plugin

```ts{}[/server/plugins/my-plugin.ts]
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-security:routeRules', (appSecurityOptions) => {
// Your code here
})
})
```

- The `appSecurityOptions` variable contains all your application security options in the form of router definitions.

These router definitions are provided in the form of h3's radix router rules.
Please see [radix3](https://github.com/unjs/radix3) for further details.

- The anonymous function `(appSecurityOptions) => {}` will be called asynchronously

If you need to fetch your security data from an external secrets' manager API, you can use `async/await` in your code.

- Your code can modify any security option inside this hook

For each route, you can modify the rules exactly as you would do it with the `routeRules` option of `nuxt.config.ts`.


### Examples

You can apply custom settings to your whole application by modifying the base `/**` route :

```ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-security:routeRules', async(appSecurityOptions) => {
const cspConnectSrc = await $fetch('https://secret-manager-api.com/api-route')
// This example replaces only the connect-src CSP directive
appSecurityOptions['/**'] = defuReplaceArray(
{
headers: {
contentSecurityPolicy: {
"connect-src": [cspConnectSrc]
}
}
},
appSecurityOptions['/**']
)
})
})
```

You can also apply your settings to selected sub-routes of your application :

```ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-security:routeRules', async(appSecurityOptions) => {
const cspConnectSrc = await $fetch('https://secret-manager-api.com/api-route')
// This example modifies the CSP only for `/admin/**` routes
appSecurityOptions['/admin/**'] = defuReplaceArray(
{
headers: {
contentSecurityPolicy: {
"connect-src": [cspConnectSrc]
}
}
},
appSecurityOptions['/admin/**']
)
})
})
```

You are not constrained to CSP options, you can modify any security option with this hook :

```ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-security:routeRules', async(appSecurityOptions) => {
const tokenLimit = await $fetch('https://secret-manager-api.com/api-route')
// This example modifies the Rate Limiter only for API routes
// It also modifies the X-Powered-By setting for these routes
appSecurityOptions['/api/**'] = defuReplaceArray(
{
rateLimiter: {
tokensPerInterval: tokenLimit
},
hidePoweredBy: false
},
appSecurityOptions['/api/**']
)
})
})
```

## Prerendered Headers Hook

The `nuxt-security:prerenderedHeaders` hook is convenient when you want to know the security headers that should be delivered by your static server.

This happens most frequently when you deploy your website statically on a CDN. In that case, your server has the HTML pages, but it doesn't know which security headers should be delivered.

You may want to configure your hosting provider so that the correct headers are delivered for each static page.


::alert{type="info"}
Your `nuxt-security:prerenderedHeaders` hook will be invoked each time your build your application.
::

### How to use

In order to use this hook, you will need write your code in `defineNuxtConfig`

```ts{}[nuxt.config.ts]
export default defineNuxtConfig({
hooks: {
'nuxt-security:prerenderedHeaders': (prerenderedHeaders) => {
// Your code here
}
}
})
```

- The `prerenderedHeaders` variable contains all calculated headers for each page.

```js
{
'/page1': {
header1: value1,
header2: value2
},
'/page2': {
header3: value3,
...
}
}
```

- The anonymous function `(prerenderedHeaders) => {}` will be called asynchronously

If you need to write to files asynchronously in your code, you can use `async/await` in your code.


### Examples

You can generate nginx-compatible header rules within your CI/CD pipeline and save them to a file on disk :

```ts{}[nuxt.config.ts]
import { writeFile } from 'node:fs/promises'
defineNuxtConfig({
hooks: {
'nuxt-security:prerenderedHeaders': async(prerenderedHeaders) => {
// Don't take this snippet for granted, this is just provided as a basic example
let nginxText = ''
for (const path in prerenderedHeaders) {
nginxText += 'location ' + path + ' {\n'
const headersForPath = prerenderedHeaders[path]
for (const headerName in headersForPath) {
const headerValue = headersForPath[headerName]
nginxText += ` add_header ${headerName} "${headerValue}";\n`
}
nginxText += '}\n\n'
}
await writeFile('./.nuxt/server.headers', nginxText)
}
}
})
```

0 comments on commit 47ed844

Please sign in to comment.