diff --git a/backend/app.js b/backend/app.js index 643f31c..9326add 100644 --- a/backend/app.js +++ b/backend/app.js @@ -2,6 +2,8 @@ import bodyParser from "body-parser"; import compression from "compression"; import express from "express"; import fileUpload from "express-fileupload"; +import helmet from "helmet"; +import { rateLimit } from "express-rate-limit"; import { isDebugMode } from "./lib/config.js"; import cors from "./lib/express/cors.js"; import jwt from "./lib/express/jwt.js"; @@ -36,25 +38,51 @@ if (isDebugMode()) { // CORS for everything app.use(cors); -// General security/cache related headers + server header -app.use((_, res, next) => { - let x_frame_options = "DENY"; - - if (typeof process.env.X_FRAME_OPTIONS !== "undefined" && process.env.X_FRAME_OPTIONS) { - x_frame_options = process.env.X_FRAME_OPTIONS; - } - - res.set({ - "X-XSS-Protection": "1; mode=block", - "X-Content-Type-Options": "nosniff", - "X-Frame-Options": x_frame_options, - "Cache-Control": "no-cache, no-store, max-age=0, must-revalidate", - Pragma: "no-cache", - Expires: 0, - }); - next(); +/** + * Global Rate Limiter: 100 requests per minute per IP + */ +const globalLimiter = rateLimit({ + windowMs: 1 * 60 * 1000, // 1 minute + max: 100, + standardHeaders: true, + legacyHeaders: false, + message: { error: { message: "Too many requests, please try again later." } }, }); +/** + * Login Rate Limiter: 10 requests per 15 minutes per IP + */ +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, + standardHeaders: true, + legacyHeaders: false, + message: { error: { message: "Too many login attempts, please try again in 15 minutes." } }, +}); + +/** + * Helmet Security Headers + */ +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // unsafe-inline/eval required by some React libs + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", "data:", "https://www.gravatar.com"], + connectSrc: ["'self'"], + frameAncestors: [process.env.X_FRAME_OPTIONS === "ALLOWALL" ? "*" : "'none'"], + }, + }, + crossOriginResourcePolicy: { policy: "cross-origin" }, + }), +); + +// Apply rate limiting +app.use("/api/tokens", loginLimiter); +app.use("/api/", globalLimiter); + // Bypass JWT for public authenticated requests mapped by WireGuard IP app.use("/wg-public", wgPublicRoutes); diff --git a/backend/package.json b/backend/package.json index 9749168..4f72124 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,7 +23,9 @@ "compression": "^1.8.1", "express": "^5.2.1", "express-fileupload": "^1.5.2", + "express-rate-limit": "^7.5.0", "gravatar": "^1.8.2", + "helmet": "^8.0.0", "jsonwebtoken": "^9.0.3", "knex": "3.1.0", "liquidjs": "10.24.0",