feat(security): implement rate limiting and helmet security headers
All checks were successful
Docker Cloud Build / Build & Publish Image (push) Successful in 11m11s

- Add express-rate-limit: login limiter (10 req/15m) and global limiter (100 req/m)
- Add helmet: secure HTTP headers with custom CSP configuration
- Remove manual header settings in favor of helmet
This commit is contained in:
xtcnet 2026-03-19 00:11:31 +07:00
parent 547360d0e3
commit 554130afbb
2 changed files with 47 additions and 17 deletions

View file

@ -2,6 +2,8 @@ import bodyParser from "body-parser";
import compression from "compression"; import compression from "compression";
import express from "express"; import express from "express";
import fileUpload from "express-fileupload"; import fileUpload from "express-fileupload";
import helmet from "helmet";
import { rateLimit } from "express-rate-limit";
import { isDebugMode } from "./lib/config.js"; import { isDebugMode } from "./lib/config.js";
import cors from "./lib/express/cors.js"; import cors from "./lib/express/cors.js";
import jwt from "./lib/express/jwt.js"; import jwt from "./lib/express/jwt.js";
@ -36,25 +38,51 @@ if (isDebugMode()) {
// CORS for everything // CORS for everything
app.use(cors); app.use(cors);
// General security/cache related headers + server header /**
app.use((_, res, next) => { * Global Rate Limiter: 100 requests per minute per IP
let x_frame_options = "DENY"; */
const globalLimiter = rateLimit({
if (typeof process.env.X_FRAME_OPTIONS !== "undefined" && process.env.X_FRAME_OPTIONS) { windowMs: 1 * 60 * 1000, // 1 minute
x_frame_options = process.env.X_FRAME_OPTIONS; max: 100,
} standardHeaders: true,
legacyHeaders: false,
res.set({ message: { error: { message: "Too many requests, please try again later." } },
"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();
/**
* 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 // Bypass JWT for public authenticated requests mapped by WireGuard IP
app.use("/wg-public", wgPublicRoutes); app.use("/wg-public", wgPublicRoutes);

View file

@ -23,7 +23,9 @@
"compression": "^1.8.1", "compression": "^1.8.1",
"express": "^5.2.1", "express": "^5.2.1",
"express-fileupload": "^1.5.2", "express-fileupload": "^1.5.2",
"express-rate-limit": "^7.5.0",
"gravatar": "^1.8.2", "gravatar": "^1.8.2",
"helmet": "^8.0.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"knex": "3.1.0", "knex": "3.1.0",
"liquidjs": "10.24.0", "liquidjs": "10.24.0",