From effe6dbfd5fb640e634f005212964c525f25d77e Mon Sep 17 00:00:00 2001 From: rishab11250 Date: Sat, 20 Jun 2026 22:42:57 +0530 Subject: [PATCH] security: add rate limiting to /api/user/:username endpoint (#222) Install express-rate-limit v8.5.2 and apply a rate limiter (30 req/min per IP, configurable via API_RATE_LIMIT env var) to the API endpoint. Also sets trust proxy for correct client IP detection behind Render.com's reverse proxy. Returns Retry-After header and JSON error on 429. --- package-lock.json | 28 ++++++++++++++++++++++++++++ package.json | 1 + server.js | 20 ++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3d201b72..adf840be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.10.0", "cors": "^2.8.5", "express": "^5.1.0", + "express-rate-limit": "^8.5.2", "helmet": "^8.2.0" }, "devDependencies": { @@ -374,6 +375,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -630,6 +649,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index d22495e7..96c1430b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "axios": "^1.10.0", "cors": "^2.8.5", "express": "^5.1.0", + "express-rate-limit": "^8.5.2", "helmet": "^8.2.0" }, "devDependencies": { diff --git a/server.js b/server.js index 7d1be02a..8ab527e7 100644 --- a/server.js +++ b/server.js @@ -5,12 +5,16 @@ const path = require("path"); const fs = require("fs"); const crypto = require("crypto"); const fetchUserInfo = require("./scripts/fetch-user-info"); +const { rateLimit } = require("express-rate-limit"); const app = express(); const PORT = process.env.PORT || 3000; app.use(cors()); +// Trust Render.com proxy so req.ip returns real client IP +app.set("trust proxy", 1); + // 1. Per-request nonce generator (used by CSP and HTML nonce injection) app.use((req, res, next) => { res.locals.nonce = crypto.randomBytes(16).toString("base64url"); @@ -124,6 +128,22 @@ app.get("/user/:username", (req, res) => { serveHtml(res, path.join(__dirname, "frontend", "user.html")); }); +// ---- Rate limiter for API endpoint ---- +const apiLimiter = rateLimit({ + windowMs: 60 * 1000, // 1-minute window + limit: parseInt(process.env.API_RATE_LIMIT, 10) || 30, + standardHeaders: "draft-8", + legacyHeaders: false, + message: { error: "Rate limit exceeded", retryAfter: 60 }, + handler: (req, res, next, options) => { + res.status(options.statusCode); + res.set("Retry-After", Math.ceil(options.windowMs / 1000)); + res.json(options.message); + }, +}); + +app.use("/api/user/:username", apiLimiter); + app.get("/api/user/:username", async (req, res) => { const username = req.params.username;