CSRF prevention

This commit is contained in:
Owen Schwartz 2024-12-25 22:04:20 -05:00
parent 993eab5ac1
commit 4e4b8744b5
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
3 changed files with 43 additions and 8 deletions

View file

@ -6,11 +6,12 @@ import logger from "@server/logger";
import { import {
errorHandlerMiddleware, errorHandlerMiddleware,
notFoundMiddleware, notFoundMiddleware,
rateLimitMiddleware, rateLimitMiddleware
} from "@server/middlewares"; } from "@server/middlewares";
import { authenticated, unauthenticated } from "@server/routers/external"; import { authenticated, unauthenticated } from "@server/routers/external";
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { logIncomingMiddleware } from "./middlewares/logIncoming";
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
import helmet from "helmet"; import helmet from "helmet";
const dev = process.env.ENVIRONMENT !== "prod"; const dev = process.env.ENVIRONMENT !== "prod";
@ -25,13 +26,22 @@ export function createApiServer() {
apiServer.use( apiServer.use(
cors({ cors({
origin: `http://localhost:${config.server.next_port}`, origin: `http://localhost:${config.server.next_port}`,
credentials: true, credentials: true
}), })
); );
} else { } else {
apiServer.use(cors()); const corsOptions = {
origin: config.app.base_url,
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: ["Content-Type", "X-CSRF-Token"],
credentials: true
};
apiServer.use(cors(corsOptions));
apiServer.use(helmet()); apiServer.use(helmet());
apiServer.use(csrfProtectionMiddleware);
} }
apiServer.use(cookieParser()); apiServer.use(cookieParser());
apiServer.use(express.json()); apiServer.use(express.json());
@ -40,8 +50,8 @@ export function createApiServer() {
rateLimitMiddleware({ rateLimitMiddleware({
windowMin: config.rate_limits.global.window_minutes, windowMin: config.rate_limits.global.window_minutes,
max: config.rate_limits.global.max_requests, max: config.rate_limits.global.max_requests,
type: "IP_AND_PATH", type: "IP_AND_PATH"
}), })
); );
} }
@ -62,7 +72,7 @@ export function createApiServer() {
const httpServer = apiServer.listen(externalPort, (err?: any) => { const httpServer = apiServer.listen(externalPort, (err?: any) => {
if (err) throw err; if (err) throw err;
logger.info( logger.info(
`API server is running on http://localhost:${externalPort}`, `API server is running on http://localhost:${externalPort}`
); );
}); });

View file

@ -0,0 +1,24 @@
import { NextFunction, Request, Response } from "express";
export function csrfProtectionMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const csrfToken = req.headers["x-csrf-token"];
// Skip CSRF check for GET requests as they should be idempotent
if (req.method === "GET") {
next();
return;
}
if (!csrfToken || csrfToken !== "x-csrf-protection") {
res.status(403).json({
error: "CSRF token missing or invalid"
});
return;
}
next();
}

View file

@ -32,7 +32,8 @@ export function createApiClient({ env }: { env: env }): AxiosInstance {
baseURL, baseURL,
timeout: 10000, timeout: 10000,
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
"X-CSRF-Token": "x-csrf-protection"
} }
}); });