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(chore): Headers per route #304

Merged
merged 9 commits into from
Dec 4, 2023
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node: [16]
node: [18]

steps:
- uses: actions/setup-node@v3
Expand All @@ -27,7 +27,7 @@ jobs:
uses: actions/checkout@master

- name: cache node_modules
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: node_modules
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ interface ModuleOptions {
enabled: boolean;
csrf: CsrfOptions | false;
nonce: boolean;
removeLoggers?: RemoveOptions | false;
ssg?: Ssg;
removeLoggers: RemoveOptions | false;
ssg: Ssg | false;
sri: boolean;
}
```
Expand Down
68 changes: 57 additions & 11 deletions docs/content/1.documentation/1.getting-started/3.usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,20 @@ export default defineNuxtConfig({
routeRules: {
'/custom-route': {
headers: {
// certain header
'Foo': 'Bar'
/* DO NOT DEFINE SECURITY HEADERS HERE
'Cross-Origin-Embedder-Policy': 'require-corp'
},
*/
}

// certain middleware
security: {
// INSTEAD USE THE CUSTOM NUXT-SECURITY PROPERTY
headers: {
// certain header
crossOriginEmbedderPolicy: 'require-corp'
},

// certain middleware
rateLimiter: {
// options
}
Expand All @@ -57,12 +65,50 @@ export default defineNuxtConfig({
```

::alert{type="warning"}
When using `routeRules`, make sure to:

1. use the proper HTTP Header names like `Cross-Origin-Embedder-Policy` instead of `crossOriginEmbedderPolicy` and to not set the headers inside `security`. These headers are handled by Nuxt and you can check more [here](https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering).
2. add middleware inside of `security` in certain route rule. This is a custom NuxtSecurity addition that does not exists in core Nuxt.
When using `routeRules`, do not use the standard `headers` property to define Nuxt Security options.
<br>
Instead, make sure to use the `security` property. This is a custom NuxtSecurity addition that does not exists in core Nuxt.
<br>
If your application defines conflicting headers at both levels, the `security` property will take precedence.
::

For more information on `routeRules` please see the [Nuxt documentation](https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering)

## Nested route configuration

Nuxt Security will recursively resolve nested routes using your `routeRules` definitions:

```ts
export default defineNuxtConfig({
// Global
security: {
headers: {
crossOriginEmbedderPolicy: 'require-corp' // By default, COEP is 'require-corp'
}
}
// Per route
routeRules: {
'/some-prefix/**': {
security: {
headers: {
crossOriginEmbedderPolicy: false // COEP disabled on all routes beginning with /some-prefix/
}
}
},
'/some-prefix/some-route': {
security: {
headers: {
crossOriginEmbedderPolicy: 'credentialless' // COEP is 'credentialless' on /some-prefix/some-route
}
}
}
}
})
```


## Inline route configuration

You can also use route roules in pages like following:

```vue
Expand All @@ -72,10 +118,10 @@ You can also use route roules in pages like following:

<script setup lang="ts">
defineRouteRules({
headers: {
'X-XSS-Protection': '1'
},
security: {
headers: {
xXSSProtection: '1'
},
rateLimiter: {
tokensPerInterval: 3,
interval: 60000,
Expand All @@ -86,7 +132,7 @@ defineRouteRules({
```

::alert{type="warning"}
To enable this macro, add following configuration to your `nuxt.config.ts` file:
To enable this macro, add the following configuration to your `nuxt.config.ts` file:

```ts
experimental: {
Expand Down
148 changes: 115 additions & 33 deletions docs/content/1.documentation/2.headers/1.csp.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ export default defineNuxtConfig({
// Per route
routeRules: {
'/custom-route': {
headers: {
'Content-Security-Policy': 'default-src 'self'; img-src https:; child-src 'none';'
security: {
headers: {
contentSecurityPolicy: <OPTIONS>,
},
},
}
}
Expand All @@ -43,7 +45,7 @@ You can also disable this header by `contentSecurityPolicy: false`.
By default, Nuxt Security will set following value for this header:

```http
Content-Security-Policy: base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic' 'nonce-{{nonce}}'; upgrade-insecure-requests
Content-Security-Policy: base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic' 'nonce-{{nonce}}'; upgrade-insecure-requests;
```

## Available values
Expand All @@ -52,34 +54,46 @@ The `contentSecurityPolicy` header can be configured with following values.

```ts
contentSecurityPolicy: {
'child-src'?: CSPSourceValue[] | false;
'connect-src'?: CSPSourceValue[] | false;
'default-src'?: CSPSourceValue[] | false;
'font-src'?: CSPSourceValue[] | false;
'frame-src'?: CSPSourceValue[] | false;
'img-src'?: CSPSourceValue[] | false;
'manifest-src'?: CSPSourceValue[] | false;
'media-src'?: CSPSourceValue[] | false;
'object-src'?: CSPSourceValue[] | false;
'prefetch-src'?: CSPSourceValue[] | false;
'script-src'?: CSPSourceValue[] | false;
'script-src-elem'?: CSPSourceValue[] | false;
'script-src-attr'?: CSPSourceValue[] | false;
'style-src'?: CSPSourceValue[] | false;
'style-src-elem'?: CSPSourceValue[] | false;
'style-src-attr'?: CSPSourceValue[] | false;
'worker-src'?: CSPSourceValue[] | false;
'base-uri'?: CSPSourceValue[] | false;
'sandbox'?: CSPSandboxValue[] | false;
'form-action'?: CSPSourceValue[] | false;
'frame-ancestors'?: ("'self'" | "'none'" | string)[] | false;
'navigate-to'?: ("'self'" | "'none'" | "'unsafe-allow-redirects'" | string)[] | false;
'report-uri'?: string[] | false;
'report-to'?: string[] | false;
'child-src'?: CSPSourceValue[] | string | false;
'connect-src'?: CSPSourceValue[] | string | false;
'default-src'?: CSPSourceValue[] | string | false;
'font-src'?: CSPSourceValue[] | string | false;
'frame-src'?: CSPSourceValue[] | string | false;
'img-src'?: CSPSourceValue[] | string | false;
'manifest-src'?: CSPSourceValue[] | string | false;
'media-src'?: CSPSourceValue[] | string | false;
'object-src'?: CSPSourceValue[] | string | false;
'prefetch-src'?: CSPSourceValue[] | string | false;
'script-src'?: CSPSourceValue[] | string | false;
'script-src-elem'?: CSPSourceValue[] | string | false;
'script-src-attr'?: CSPSourceValue[] | string | false;
'style-src'?: CSPSourceValue[] | string | false;
'style-src-elem'?: CSPSourceValue[] | string | false;
'style-src-attr'?: CSPSourceValue[] | string | false;
'worker-src'?: CSPSourceValue[] | string | false;
'base-uri'?: CSPSourceValue[] | string | false;
'sandbox'?: CSPSandboxValue[] | string | false;
'form-action'?: CSPSourceValue[] | string | false;
'frame-ancestors'?: ("'self'" | "'none'" | string)[] | string | false;
'navigate-to'?: ("'self'" | "'none'" | "'unsafe-allow-redirects'" | string)[] | string | false;
'report-uri'?: string[] | string | false;
'report-to'?: string | false;
'upgrade-insecure-requests'?: boolean;
} | false
```

::callout
#summary
Array and String syntaxes
#content
Directives can be written using the array syntax or the string syntax.

- Array syntax for clear definition of policies: `"script-src": ["'self'", "https:", "'unsafe-inline'"]`
- String syntax if you prefer to stick with native MDN syntax: `"script-src": "'self' https: 'unsafe-inline'"`

Please note that these two syntaxes behave differently for deeply nested route definitions: see [Per-route Configuration](#per-route-configuration)
::

::callout
#summary
CSPSourceValue type
Expand Down Expand Up @@ -123,6 +137,7 @@ type CSPSandboxValue =
```
::


## Strict CSP

Nuxt Security helps you increase the security of your site by enabling **Strict CSP** support for both SSR and SSG applications.
Expand Down Expand Up @@ -181,6 +196,7 @@ In SSR mode, Strict CSP is enabled when you set the `nonce` option and the `"'no

```ts
export default defineNuxtConfig({
// Global
security: {
nonce: true, // Enables HTML nonce support in SSR mode
},
Expand All @@ -191,6 +207,20 @@ export default defineNuxtConfig({
"'nonce-{{nonce}}'" // Enables CSP nonce support for scripts in SSR mode, supported by almost any browser (level 2)
]
}
},

// Per route
routeRules: {
'/custom-route': {
security: {
nonce: false,
headers: {
contentSecurityPolicy: {
'script-src': "self 'unsafe-inline'"
},
},
},
}
}
})
```
Expand Down Expand Up @@ -230,6 +260,7 @@ For SSG apps, Strict CSP is enabled when you set the `ssg` and `sri` options:

```ts
export default defineNuxtConfig({
// Global
security: {
ssg: {
hashScripts: true, // Enables CSP hash support for scripts in SSG mode
Expand All @@ -244,6 +275,21 @@ export default defineNuxtConfig({
]
}
}
},

// Per route
routeRules: {
'/custom-route': {
security: {
ssg: false,
sri: false,
headers: {
contentSecurityPolicy: {
'script-src': "self 'unsafe-inline'"
},
},
},
}
}
})
```
Expand Down Expand Up @@ -290,24 +336,60 @@ export default defineNuxtConfig({
})
```

Note that this is not necessary if you use our default configuration settings.

## Per-route configuration

The `nonce` value is generated per request and is added to the CSP header. This behaviour can be tweaked on a route level by using the `routeRules` option:

All Content Security Policy options can be defined on a per-route level.
```ts
export default defineNuxtConfig({
// Global
security: {
headers: {
contentSecurityPolicy: {
'img-src': false // By default, no images can be loaded
}
}
}
// Per route
routeRules: {
'/custom-route': {
nonce: false // do not generate nonce for this route (1)
'/some-prefix/**': {
security: {
headers: {
contentSecurityPolicy: {
'img-src': ["'self'"] // Self-hosted images can be loaded for routes beginning with /some-prefix/
}
}
}
},
'/other-route': {
nonce: true // generate a new nonce for this route (2)
'/some-prefix/some-route/**': {
security: {
headers: {
contentSecurityPolicy: { // With array syntax : additive
'img-src': ["https:"] // Self-hosted AND https: images can be loaded for routes beginning with /some-prefix/some-route/
}
}
}
},
'/some-prefix/some-route/some-page': {
security: {
headers: {
contentSecurityPolicy: { // With string syntax : substitutive
'img-src': 'self' // ONLY self-hosted images can be loaded on /some-prefix/some-route/some-page
}
}
}
}
}
})
```

Nuxt Security resolves the `contentSecurityPolicy` options using the native Nitro router strategy:
- **Additive merging** with the array syntax: If you write your rules with the array syntax (e.g. `"img-src": ["'self'", "https:"]`), the new route policies will be added to the policies defined for higher-level routes.
Use this strategy if you need to add specific policy values to your route without deleting the existing ones.

- **Substitutive merging** with the string syntax: If you write your rules with the string syntax (e.g. `"img-src": "'self' https:"`), the new route policies will be substituted to the policies defined for higher-level routes.
Use this strategy if you need to delete existing policies before setting your specific route policies.

## Nonces for SSR

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ export default defineNuxtConfig({
// Per route
routeRules: {
'/custom-route': {
headers: {
'X-DNS-Prefetch-Control': <OPTIONS>
security: {
headers: {
xDNSPrefetchControl: <OPTIONS>,
},
},
}
}
Expand Down
6 changes: 4 additions & 2 deletions docs/content/1.documentation/2.headers/11.xDownloadOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ export default defineNuxtConfig({
// Per route
routeRules: {
'/custom-route': {
headers: {
'X-Download-Options': <OPTIONS>
security: {
headers: {
xDownloadOptions: <OPTIONS>,
},
},
}
}
Expand Down
6 changes: 4 additions & 2 deletions docs/content/1.documentation/2.headers/12.xFrameOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ export default defineNuxtConfig({
// Per route
routeRules: {
'/custom-route': {
headers: {
'X-Frame-Options': <OPTIONS>
security: {
headers: {
xFrameOptions: <OPTIONS>,
},
},
}
}
Expand Down
Loading
Loading