D3V-Server/backend/app.js
xtcnet 554130afbb
All checks were successful
Docker Cloud Build / Build & Publish Image (push) Successful in 11m11s
feat(security): implement rate limiting and helmet security headers
- 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
2026-03-19 00:11:31 +07:00

124 lines
3.1 KiB
JavaScript

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";
import { debug, express as logger } from "./logger.js";
import mainRoutes from "./routes/main.js";
import wgPublicRoutes from "./routes/wg_public.js";
/**
* App
*/
const app = express();
app.use(fileUpload());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Gzip
app.use(compression());
/**
* General Logging, BEFORE routes
*/
app.disable("x-powered-by");
app.enable("trust proxy", ["loopback", "linklocal", "uniquelocal"]);
app.enable("strict routing");
// pretty print JSON when not live
if (isDebugMode()) {
app.set("json spaces", 2);
}
// CORS for everything
app.use(cors);
/**
* 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);
app.use(jwt());
app.use("/", mainRoutes);
// production error handler
// no stacktraces leaked to user
app.use((err, req, res, _) => {
const payload = {
error: {
code: err.status,
message: err.public ? err.message : "Internal Error",
},
};
if (typeof err.message_i18n !== "undefined") {
payload.error.message_i18n = err.message_i18n;
}
if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) {
payload.debug = {
stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null,
previous: err.previous,
};
}
// Not every error is worth logging - but this is good for now until it gets annoying.
if (typeof err.stack !== "undefined" && err.stack) {
debug(logger, err.stack);
if (typeof err.public === "undefined" || !err.public) {
logger.warn(err.message);
}
}
res.status(err.status || 500).send(payload);
});
export default app;