Skip to content

Commit

Permalink
support middleware and nonce (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
Enalmada authored Oct 6, 2023
1 parent 7a71c8d commit 5d4660c
Show file tree
Hide file tree
Showing 8 changed files with 439 additions and 83 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-kids-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@enalmada/next-secure': minor
---

support middleware and nonce
67 changes: 65 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,77 @@ bun add @enalmada/next-secure

## Usage

### In next.config.cjs
### In middleware

1) Define your rules. See [next-safe](https://github.com/trezy/next-safe) for config values
```ts
// cspRules.ts
import { type ContentSecurityPolicyTemplate, type CspRule } from '@enalmada/next-secure';

const isDev = process.env.NODE_ENV === 'development';

export const cspConfig: ContentSecurityPolicyTemplate = {
isDev,
contentSecurityPolicy: {
mergeDefaultDirectives: true,
'prefetch-src': false, // shouldn't be used
},
// https://web.dev/referrer-best-practices/
referrerPolicy: 'strict-origin-when-cross-origin',
// These "false" are included in proposed/standard but cause chrome noise. Disabling for now.
permissionsPolicy: {
'ambient-light-sensor': false,
battery: false,
'document-domain': false,
'execution-while-not-rendered': false,
'execution-while-out-of-viewport': false,
'navigation-override': false,
'speaker-selection': false,
},
permissionsPolicyDirectiveSupport: ['proposed', 'standard'], // default causes tons of console noise
};

export const cspRules: CspRule[] = [
{ description: 'react-dev', 'object-src': isDev ? 'data:' : undefined, source: '/:path*' },
{
description: 'firebase',
'script-src': 'https://apis.google.com/ https://accounts.google.com/gsi/client',
'connect-src':
'https://apis.google.com https://accounts.google.com/gsi/ https://securetoken.googleapis.com https://identitytoolkit.googleapis.com https://lh3.googleusercontent.com',
'img-src': 'https://lh3.googleusercontent.com',
'frame-src': `https://accounts.google.com/gsi/ https://securetoken.googleapis.com https://identitytoolkit.googleapis.com https://${process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN}/`,
source: '/:path*',
},
{
description: 'sentry',
'worker-src': 'blob:',
'connect-src': 'https://oxxxxx.ingest.sentry.io',
source: '/:path*',
},
];
```

2) use `generateSecurityHeaders` to create headers and `applyHeaders` to add them to response.
```ts

import { applyHeaders, generateSecurityHeaders } from '@enalmada/next-secure';
import { cspConfig, cspRules } from '@/cspRules';

export async function middleware(request: NextRequest) {
const secureHeaders = generateSecurityHeaders(cspConfig, cspRules);
...
const result = NextResponse.next(); // or intlMiddleware(request); etc
return applyHeaders(result, secureHeaders);
```
### In next.config.cjs (deprecated)
```ts
// next.config.mjs
// @ts-check


const isDev = process.env.NODE_ENV !== 'production';
const isDev = process.env.NODE_ENV === 'development';

/** @type {import("@enalmada/next-secure").ContentSecurityPolicyTemplate} */
const cspConfig = {
Expand Down
Binary file modified bun.lockb
Binary file not shown.
6 changes: 4 additions & 2 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export declare const Default: NextSafeConfig;
import { type NextResponse } from 'next/server';
export interface CspRule {
description?: string;
source?: string;
Expand Down Expand Up @@ -38,8 +38,10 @@ export type SourceHeaders = {
source: string;
headers: string;
};
declare function generateCspTemplates(cspConfig: ContentSecurityPolicyTemplate, cspRules: CspRule[], keysToRemove?: string[]): {
declare function generateCspTemplates(cspConfig: ContentSecurityPolicyTemplate, cspRules: CspRule[], keysToRemove?: string[], nonce?: string): {
source: string;
headers: Header[];
}[];
export declare function generateSecurityHeaders(cspConfig: ContentSecurityPolicyTemplate, cspRules: CspRule[], keysToRemove?: string[], nonce?: string): Header[];
export declare function applyHeaders(response: NextResponse, headers: Header[]): NextResponse;
export { generateCspTemplates };
99 changes: 62 additions & 37 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ var deepMerge = function(target, source) {
var isObject = function(item) {
return item && typeof item === "object" && !Array.isArray(item);
};
var groupBySource = function(cspRules) {
var groupBySource = function(cspRules, ignorePath = false) {
const grouped = {};
cspRules.forEach((rule) => {
const source = rule.source || "/";
const source = ignorePath ? "/" : rule.source || "/";
if (!grouped[source]) {
grouped[source] = [];
}
grouped[source].push(rule);
});
return grouped;
};
var generateCspTemplate = function(cspConfig, cspRules) {
const groupedRules = groupBySource(cspRules);
var generateCspTemplate = function(cspConfig, cspRules, nonce) {
const groupedRules = groupBySource(cspRules, !!nonce);
const finalConfigs = [];
for (const [source, rules] of Object.entries(groupedRules)) {
const finalConfig = deepMerge(Default, cspConfig);
const finalConfig = deepMerge(getDefaultConfig(cspConfig.isDev, nonce), cspConfig);
const generatedCsp = { ...finalConfig.contentSecurityPolicy };
rules.forEach((rule) => {
for (const [key, value] of Object.entries(rule)) {
Expand Down Expand Up @@ -69,47 +69,72 @@ var generateCspTemplate = function(cspConfig, cspRules) {
}
return finalConfigs;
};
var generateCspTemplates = function(cspConfig, cspRules, keysToRemove = defaultKeysToRemove) {
const contentSecurityPolicyTemplates = generateCspTemplate(cspConfig, cspRules);
var generateCspTemplates = function(cspConfig, cspRules, keysToRemove = defaultKeysToRemove, nonce) {
const contentSecurityPolicyTemplates = generateCspTemplate(cspConfig, cspRules, nonce);
return contentSecurityPolicyTemplates.map((template) => {
return {
source: template.source || "/:path*",
headers: nextSafe.default({ ...template }).filter((header) => !keysToRemove.includes(header.key))
};
});
};
var Default = {
contentTypeOptions: "nosniff",
contentSecurityPolicy: {
"base-uri": "'none'",
"child-src": "'none'",
"connect-src": "'self'",
"default-src": "'self'",
"font-src": "'self'",
"form-action": "'self'",
"frame-ancestors": "'none'",
"frame-src": "'none'",
"img-src": "'self'",
"manifest-src": "'self'",
"media-src": "'self'",
"object-src": "'none'",
"prefetch-src": "'self'",
"script-src": "'self'",
"style-src": "'self'",
"worker-src": "'self'",
"block-all-mixed-content": true,
"upgrade-insecure-requests": true,
mergeDefaultDirectives: false,
reportOnly: false
},
frameOptions: "DENY",
permissionsPolicyDirectiveSupport: ["proposed", "standard"],
isDev: false,
referrerPolicy: "no-referrer",
xssProtection: "1; mode=block"
function generateSecurityHeaders(cspConfig, cspRules, keysToRemove = defaultKeysToRemove, nonce) {
if (!nonce) {
nonce = Buffer.from(crypto.randomUUID()).toString("base64");
}
const templates = generateCspTemplates(cspConfig, cspRules, keysToRemove, nonce);
const headersFromTemplate = templates.shift()?.headers || [];
headersFromTemplate.push({ key: "x-nonce", value: nonce });
return headersFromTemplate;
}
function applyHeaders(response, headers) {
headers.forEach((header) => {
response.headers.set(header.key, header.value);
});
return response;
}
var getDefaultConfig = (isDev, nonce) => {
let scriptSrc = "'self'";
let styleSrc = "'self'";
if (isDev) {
scriptSrc += " 'unsafe-inline'";
styleSrc += " 'unsafe-inline'";
} else if (nonce) {
scriptSrc += ` 'nonce-${nonce}' 'strict-dynamic'`;
styleSrc += ` 'nonce-${nonce}'`;
}
return {
contentTypeOptions: "nosniff",
contentSecurityPolicy: {
"base-uri": "'none'",
"child-src": "'none'",
"connect-src": "'self'",
"default-src": "'self'",
"font-src": "'self'",
"form-action": "'self'",
"frame-ancestors": "'none'",
"frame-src": "'none'",
"img-src": "'self'",
"manifest-src": "'self'",
"media-src": "'self'",
"object-src": "'none'",
"prefetch-src": "'self'",
"script-src": scriptSrc,
"style-src": styleSrc,
"worker-src": "'self'",
mergeDefaultDirectives: false,
reportOnly: false
},
frameOptions: "DENY",
permissionsPolicyDirectiveSupport: ["proposed", "standard"],
isDev: false,
referrerPolicy: "no-referrer",
xssProtection: "1; mode=block"
};
};
var defaultKeysToRemove = ["Feature-Policy", "X-Content-Security-Policy", "X-WebKit-CSP"];
export {
generateSecurityHeaders,
generateCspTemplates,
Default
applyHeaders
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"typescript": "^5.2.2",
"vitest": "^0.34.6"
},
"peerDependencies": {
"next": "^13.5.4"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "Adam Lane",
Expand Down
Loading

0 comments on commit 5d4660c

Please sign in to comment.