Add Strict CSP Headers (#243)
* Add CSP nonce support and enhance security headers Introduced secure headers and CSP nonce to improve app security by integrating `@nosecone/next`. Updated middleware, root providers, and layout to handle nonce propagation, enabling stricter CSP policies when configured. Also upgraded dependencies and tooling versions. * Add OTP and security guidelines documentation and additional checks on client-provided values - Introduced additional checks on client-provided values such as cookies - Introduced a new OTP API documentation outlining the creation and verification of OTP tokens for sensitive operations. - Added comprehensive security guidelines for writing secure code in Next.js, covering client and server components, environment variables, authentication, and error handling. These additions enhance the project's security posture and provide clear instructions for developers on implementing secure practices.
This commit is contained in:
committed by
GitHub
parent
903ef6dc08
commit
db9ddab6ad
@@ -1,3 +1,5 @@
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import { Toaster } from '@kit/ui/sonner';
|
||||
|
||||
import { RootProviders } from '~/components/root-providers';
|
||||
@@ -20,11 +22,12 @@ export default async function RootLayout({
|
||||
const { language } = await createI18nServerInstance();
|
||||
const theme = await getRootTheme();
|
||||
const className = getFontsClassName(theme);
|
||||
const nonce = await getCspNonce();
|
||||
|
||||
return (
|
||||
<html lang={language} className={className}>
|
||||
<body>
|
||||
<RootProviders theme={theme} lang={language}>
|
||||
<RootProviders theme={theme} lang={language} nonce={nonce}>
|
||||
{children}
|
||||
</RootProviders>
|
||||
|
||||
@@ -33,3 +36,9 @@ export default async function RootLayout({
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
async function getCspNonce() {
|
||||
const headersStore = await headers();
|
||||
|
||||
return headersStore.get('x-nonce') ?? undefined;
|
||||
}
|
||||
|
||||
@@ -37,14 +37,21 @@ const CaptchaTokenSetter = dynamic(async () => {
|
||||
};
|
||||
});
|
||||
|
||||
type RootProvidersProps = React.PropsWithChildren<{
|
||||
// The language to use for the app (optional)
|
||||
lang?: string;
|
||||
// The theme (light or dark or system) (optional)
|
||||
theme?: string;
|
||||
// The CSP nonce to pass to scripts (optional)
|
||||
nonce?: string;
|
||||
}>;
|
||||
|
||||
export function RootProviders({
|
||||
lang,
|
||||
theme = appConfig.theme,
|
||||
nonce,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
lang?: string;
|
||||
theme?: string;
|
||||
}>) {
|
||||
}: RootProvidersProps) {
|
||||
const i18nSettings = useMemo(() => getI18nSettings(lang), [lang]);
|
||||
|
||||
return (
|
||||
@@ -63,6 +70,7 @@ export function RootProviders({
|
||||
disableTransitionOnChange
|
||||
defaultTheme={theme}
|
||||
enableColorScheme={false}
|
||||
nonce={nonce}
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
|
||||
94
apps/web/lib/create-csp-response.ts
Normal file
94
apps/web/lib/create-csp-response.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { NoseconeOptions } from '@nosecone/next';
|
||||
|
||||
// we need to allow connecting to the Supabase API from the client
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL as string;
|
||||
|
||||
// the URL used for Supabase Realtime
|
||||
const WEBSOCKET_URL = SUPABASE_URL.replace('https://', 'ws://').replace(
|
||||
'http://',
|
||||
'ws://',
|
||||
);
|
||||
|
||||
// disabled to allow loading images from Supabase Storage
|
||||
const CROSS_ORIGIN_EMBEDDER_POLICY = false;
|
||||
|
||||
/**
|
||||
* @name ALLOWED_ORIGINS
|
||||
* @description List of allowed origins for the "connectSrc" directive in the Content Security Policy.
|
||||
*/
|
||||
const ALLOWED_ORIGINS = [
|
||||
SUPABASE_URL,
|
||||
WEBSOCKET_URL,
|
||||
// add here additional allowed origins
|
||||
] as never[];
|
||||
|
||||
/**
|
||||
* @name IMG_SRC_ORIGINS
|
||||
*/
|
||||
const IMG_SRC_ORIGINS = [SUPABASE_URL] as never[];
|
||||
|
||||
/**
|
||||
* @name UPGRADE_INSECURE_REQUESTS
|
||||
* @description Upgrade insecure requests to HTTPS when in production
|
||||
*/
|
||||
const UPGRADE_INSECURE_REQUESTS = process.env.NODE_ENV === 'production';
|
||||
|
||||
/**
|
||||
* @name createCspResponse
|
||||
* @description Create a middleware with enhanced headers applied (if applied).
|
||||
*/
|
||||
export async function createCspResponse() {
|
||||
const {
|
||||
createMiddleware,
|
||||
withVercelToolbar,
|
||||
defaults: noseconeConfig,
|
||||
} = await import('@nosecone/next');
|
||||
|
||||
/*
|
||||
* @name allowedOrigins
|
||||
* @description List of allowed origins for the "connectSrc" directive in the Content Security Policy.
|
||||
*/
|
||||
|
||||
const config: NoseconeOptions = {
|
||||
...noseconeConfig,
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
...noseconeConfig.contentSecurityPolicy.directives,
|
||||
connectSrc: [
|
||||
...noseconeConfig.contentSecurityPolicy.directives.connectSrc,
|
||||
...ALLOWED_ORIGINS,
|
||||
],
|
||||
imgSrc: [
|
||||
...noseconeConfig.contentSecurityPolicy.directives.imgSrc,
|
||||
...IMG_SRC_ORIGINS,
|
||||
],
|
||||
upgradeInsecureRequests: UPGRADE_INSECURE_REQUESTS,
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: CROSS_ORIGIN_EMBEDDER_POLICY,
|
||||
};
|
||||
|
||||
const middleware = createMiddleware(
|
||||
process.env.VERCEL_ENV === 'preview' ? withVercelToolbar(config) : config,
|
||||
);
|
||||
|
||||
// create response
|
||||
const response = await middleware();
|
||||
|
||||
if (response) {
|
||||
const contentSecurityPolicy = response.headers.get(
|
||||
'Content-Security-Policy',
|
||||
);
|
||||
|
||||
const matches = contentSecurityPolicy?.match(/nonce-([\w-]+)/) || [];
|
||||
const nonce = matches[1];
|
||||
|
||||
// set x-nonce header if nonce is found
|
||||
// so we can pass it to client-side scripts
|
||||
if (nonce) {
|
||||
response.headers.set('x-nonce', nonce);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -24,7 +24,8 @@ const getUser = (request: NextRequest, response: NextResponse) => {
|
||||
};
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const response = NextResponse.next();
|
||||
const secureHeaders = await createResponseWithSecureHeaders();
|
||||
const response = NextResponse.next(secureHeaders);
|
||||
|
||||
// set a unique request ID for each request
|
||||
// this helps us log and trace requests
|
||||
@@ -59,7 +60,7 @@ export async function middleware(request: NextRequest) {
|
||||
|
||||
async function withCsrfMiddleware(
|
||||
request: NextRequest,
|
||||
response = new NextResponse(),
|
||||
response: NextResponse,
|
||||
) {
|
||||
// set up CSRF protection
|
||||
const csrfProtect = createCsrfProtect({
|
||||
@@ -100,7 +101,7 @@ async function adminMiddleware(request: NextRequest, response: NextResponse) {
|
||||
const isAdminPath = request.nextUrl.pathname.startsWith('/admin');
|
||||
|
||||
if (!isAdminPath) {
|
||||
return response;
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -222,3 +223,21 @@ function matchUrlPattern(url: string) {
|
||||
function setRequestId(request: Request) {
|
||||
request.headers.set('x-correlation-id', crypto.randomUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* @name createResponseWithSecureHeaders
|
||||
* @description Create a middleware with enhanced headers applied (if applied).
|
||||
* This is disabled by default. To enable set ENABLE_STRICT_CSP=true
|
||||
*/
|
||||
async function createResponseWithSecureHeaders() {
|
||||
const enableStrictCsp = process.env.ENABLE_STRICT_CSP ?? 'false';
|
||||
|
||||
// we disable ENABLE_STRICT_CSP by default
|
||||
if (enableStrictCsp === 'false') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { createCspResponse } = await import('./lib/create-csp-response');
|
||||
|
||||
return createCspResponse();
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"@makerkit/data-loader-supabase-core": "^0.0.10",
|
||||
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@nosecone/next": "1.0.0-beta.6",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@supabase/supabase-js": "2.49.4",
|
||||
"@tanstack/react-query": "5.74.4",
|
||||
|
||||
Reference in New Issue
Block a user