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

Replace next-secure-headers with Nosecone for security headers #343

Merged
merged 8 commits into from
Dec 11, 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
5 changes: 4 additions & 1 deletion apps/app/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { authMiddleware } from '@repo/auth/middleware';
import { noseconeConfig, noseconeMiddleware } from '@repo/security/middleware';

export default authMiddleware();
const securityHeaders = noseconeMiddleware(noseconeConfig);

export default authMiddleware(() => securityHeaders());

export const config = {
matcher: [
Expand Down
2 changes: 1 addition & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
},
"dependencies": {
"@prisma/client": "6.0.1",
"@repo/auth": "workspace:*",
"@repo/analytics": "workspace:*",
"@repo/auth": "workspace:*",
"@repo/collaboration": "workspace:*",
"@repo/database": "workspace:*",
"@repo/design-system": "workspace:*",
Expand Down
7 changes: 5 additions & 2 deletions apps/web/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { authMiddleware } from '@repo/auth/middleware';
import { env } from '@repo/env';
import { parseError } from '@repo/observability/error';
import { secure } from '@repo/security';
import { noseconeConfig, noseconeMiddleware } from '@repo/security/middleware';
import { NextResponse } from 'next/server';

export const config = {
Expand All @@ -10,9 +11,11 @@ export const config = {
matcher: ['/((?!_next/static|_next/image|ingest|favicon.ico).*)'],
};

const securityHeaders = noseconeMiddleware(noseconeConfig);

export default authMiddleware(async (_auth, request) => {
if (!env.ARCJET_KEY) {
return NextResponse.next();
return securityHeaders();
}

try {
Expand All @@ -26,7 +29,7 @@ export default authMiddleware(async (_auth, request) => {
request
);

return NextResponse.next();
return securityHeaders();
} catch (error) {
const message = parseError(error);

Expand Down
107 changes: 94 additions & 13 deletions docs/features/security/headers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,106 @@ title: Security Headers
description: Security headers used to protect your application.
---

next-forge uses [next-secure-headers](https://github.com/jagaapple/next-secure-headers) to set HTTP response headers related to security.
import { Authors } from '/snippets/authors.mdx';

<Authors data={[{
user: {
name: 'Hayden Bleasel',
id: 'haydenbleasel',
},
company: {
name: 'next-forge',
id: 'next-forge',
},
}, {
user: {
name: 'David Mytton',
id: 'davidmytton',
},
company: {
name: 'Arcjet',
id: 'arcjet',
},
}]} />

next-forge uses [Nosecone](https://docs.arcjet.com/nosecone/quick-start) to set HTTP response headers related to security.

## Configuration

Here are the headers we have enabled:

| Property | Header | Description | Value |
| --- | --- | --- | --- |
| `forceHTTPSRedirect` | `Strict-Transport-Security` | Prevents browsers from connecting to your site over HTTP. | `[true, { maxAge: 63_072_000, includeSubDomains: true, preload: true }]` |
| `frameGuard` | `X-Frame-Options` | Prevents browsers from rendering your site in an iframe. | `deny` |
| `noopen` | `X-Download-Options` | Prevents browsers from automatically opening downloaded files in the same origin as the page. | `noopen` |
| `nosniff` | `X-Content-Type-Options` | Prevents browsers from MIME-sniffing a response away from the declared content type. | `nosniff` |
| `xssProtection` | `X-XSS-Protection` | Prevents browsers from executing inline scripts if a cross-site scripting attack is detected. | `sanitize` |
| `contentSecurityPolicy` | `Content-Security-Policy` | Sets a policy to prevent a wide range of different types of attacks, including Cross Site Scripting (XSS) and data injection attacks. | `false` |
| `expectCT` | `Expect-CT` | Enables a mechanism to mitigate the risk of fraudulent certificates being used in connections to your site. | `false` |
| `referrerPolicy` | `Referrer-Policy` | Controls how much of the full URL is included in the `Referer` header. | `false` |
- `Cross-Origin-Embedder-Policy` (COEP)
- `Cross-Origin-Opener-Policy`
- `Cross-Origin-Resource-Policy`
- `Origin-Agent-Cluster`
- `Referrer-Policy`
- `Strict-Transport-Security` (HSTS)
- `X-Content-Type-Options`
- `X-DNS-Prefetch-Control`
- `X-Download-Options`
- `X-Frame-Options`
- `X-Permitted-Cross-Domain-Policies`
- `X-XSS-Protection`

<Tip>The `forceHTTPSRedirect` property has been customized from the default to include subdomains and preload the HSTS policy. This should allow you to submit your site at [hstspreload.org](https://hstspreload.org/) without any issues.</Tip>
See the [Nosecone reference](https://docs.arcjet.com/nosecone/reference) for details on each header and configuration options.

## Usage

The headers are enabled by default when using the `next-config` package. If you are customizing your `next.config.ts` file, you can extend the headers manually.
Recommended headers are set by default and configured in `@repo/security/middleware`. Changing the configuration here will affect all apps.

They are then attached to the response within the middleware in `apps/app/middleware` and `apps/web/middleware.ts`. Adjusting the configuration in these files will only affect the specific app.

## Content Security Policy (CSP)

The CSP header is not set by default because it requires specific configuration based on the next-forge features you have enabled.

In the meantime, you can set the CSP header using the Nosecone configuration. For example, the following CSP configuration will work with the default next-forge features:

```ts
import type { NoseconeOptions } from '@nosecone/next';
import { defaults as noseconeDefaults } from '@nosecone/next';

const noseconeOptions: NoseconeOptions = {
...noseconeDefaults,
contentSecurityPolicy: {
...noseconeDefaults.contentSecurityPolicy,
directives: {
...noseconeDefaults.contentSecurityPolicy.directives,
scriptSrc: [
// We have to use unsafe-inline because next-themes and Vercel Analytics
// do not support nonce
// https://github.com/pacocoursey/next-themes/issues/106
// https://github.com/vercel/analytics/issues/122
//...noseconeDefaults.contentSecurityPolicy.directives.scriptSrc,
"'self'",
"'unsafe-inline'",
"https://www.googletagmanager.com",
"https://*.clerk.accounts.dev",
"https://va.vercel-scripts.com",
],
connectSrc: [
...noseconeDefaults.contentSecurityPolicy.directives.connectSrc,
"https://*.clerk.accounts.dev",
"https://*.google-analytics.com",
"https://clerk-telemetry.com",
],
workerSrc: [
...noseconeDefaults.contentSecurityPolicy.directives.workerSrc,
"blob:",
"https://*.clerk.accounts.dev"
],
imgSrc: [
...noseconeDefaults.contentSecurityPolicy.directives.imgSrc,
"https://img.clerk.com"
],
objectSrc: [
...noseconeDefaults.contentSecurityPolicy.directives.objectSrc,
],
// We only set this in production because the server may be started
// without HTTPS
upgradeInsecureRequests: process.env.NODE_ENV === "production",
},
},
}
```

17 changes: 0 additions & 17 deletions packages/next-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { env } from '@repo/env';
import { withSentryConfig } from '@sentry/nextjs';
import withVercelToolbar from '@vercel/toolbar/plugins/next';
import type { NextConfig } from 'next';
import { createSecureHeaders } from 'next-secure-headers';

const otelRegex = /@opentelemetry\/instrumentation/;

Expand Down Expand Up @@ -39,22 +38,6 @@ const baseConfig: NextConfig = {
];
},

// biome-ignore lint/suspicious/useAwait: headers is async
async headers() {
return [
{
source: '/(.*)',
headers: createSecureHeaders({
// HSTS Preload: https://hstspreload.org/
forceHTTPSRedirect: [
true,
{ maxAge: 63_072_000, includeSubDomains: true, preload: true },
],
}),
},
];
},

webpack(config, { isServer }) {
if (isServer) {
config.plugins = [...config.plugins, new PrismaPlugin()];
Expand Down
3 changes: 1 addition & 2 deletions packages/next-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"@next/bundle-analyzer": "^15.1.0",
"@prisma/nextjs-monorepo-workaround-plugin": "^6.0.1",
"@sentry/nextjs": "^8.43.0",
"@vercel/toolbar": "^0.1.28",
"next-secure-headers": "^2.2.0"
"@vercel/toolbar": "^0.1.28"
}
}
23 changes: 23 additions & 0 deletions packages/security/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
type NoseconeOptions,
defaults,
withVercelToolbar,
} from '@nosecone/next';
import { env } from '@repo/env';
export { createMiddleware as noseconeMiddleware } from '@nosecone/next';

// Nosecone security headers configuration
// https://docs.arcjet.com/nosecone/quick-start
const noseconeOptions: NoseconeOptions = {
...defaults,
// Content Security Policy (CSP) is disabled by default because the values
// depend on which Next Forge features are enabled. See
// https://docs.next-forge.com/features/security/headers for guidance on how
// to configure it.
contentSecurityPolicy: false,
};

export const noseconeConfig: NoseconeOptions =
env.NODE_ENV === 'development' && env.FLAGS_SECRET
? withVercelToolbar(noseconeOptions)
: noseconeOptions;
2 changes: 2 additions & 0 deletions packages/security/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "@repo/security",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"clean": "git clean -xdf .cache .turbo dist node_modules",
"format": "biome lint --write .",
Expand All @@ -10,6 +11,7 @@
},
"dependencies": {
"@arcjet/next": "1.0.0-alpha.34",
"@nosecone/next": "1.0.0-alpha.34",
"@repo/env": "workspace:*"
},
"devDependencies": {
Expand Down
29 changes: 20 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading