50 lines
1.3 KiB
TypeScript
50 lines
1.3 KiB
TypeScript
import { createContext } from "react";
|
|
import crypto from "node:crypto";
|
|
|
|
export interface CspMiddlewareOptions {
|
|
reportOnly?: boolean;
|
|
}
|
|
|
|
/**
|
|
* React context exposing the per-request CSP nonce.
|
|
* Default is "" — client-side components read empty string (no-op).
|
|
*/
|
|
export const CspNonceContext = createContext<string>("");
|
|
|
|
/**
|
|
* Factory returning express-style middleware that:
|
|
* 1. Generates a per-request nonce
|
|
* 2. Sets the CSP header (or Report-Only variant)
|
|
* 3. Attaches `req.cspNonce` for downstream middleware
|
|
*/
|
|
export function cspMiddleware(options?: CspMiddlewareOptions) {
|
|
const headerName = options?.reportOnly
|
|
? "Content-Security-Policy-Report-Only"
|
|
: "Content-Security-Policy";
|
|
|
|
return (
|
|
req: Record<string, unknown>,
|
|
res: { setHeader(name: string, value: string): void },
|
|
next: () => void,
|
|
): void => {
|
|
const nonce = crypto.randomUUID();
|
|
|
|
const policy = [
|
|
`default-src 'self'`,
|
|
`script-src 'self' 'nonce-${nonce}'`,
|
|
`style-src 'self' 'unsafe-inline'`,
|
|
`img-src 'self' data: https:`,
|
|
`font-src 'self'`,
|
|
`connect-src 'self' https:`,
|
|
`frame-ancestors 'self'`,
|
|
`base-uri 'self'`,
|
|
`form-action 'self'`,
|
|
].join("; ");
|
|
|
|
res.setHeader(headerName, policy);
|
|
req["cspNonce"] = nonce;
|
|
|
|
next();
|
|
};
|
|
}
|