Commit 4bb319e9 by tdgiang

Rate-limit POST /admin/login to bound bcrypt-induced event-loop stalls

Async bcryptjs measured to still not yield the event loop for realistic
hash costs (~70ms compares finish before its 100ms yield threshold), so
a login flood could still stall the same process's live MegaPay payment
webhooks. Caps each client (or shared-proxy-bucket, see known limitation
below) to 5 POST /admin/login attempts per rolling 60s window via
express-rate-limit; requests over the limit get a 429 with a Vietnamese
error and never reach adminAuth.login, so the bcrypt compare never runs.

Known limitation: this app has no app.set('trust proxy', ...) configured
and runs behind a reverse proxy in production, so express-rate-limit's
default req.ip-based bucketing will key off the proxy's address, not the
real client IP. In production this enforces "5 attempts/minute in
aggregate behind the proxy" rather than "5 per real client IP" — an
accepted trade-off for this low-traffic internal tool, but not the same
guarantee trust proxy + per-IP limiting would give. Configuring trust
proxy is an infrastructure change, left out of scope here.
parent aba86181
"use strict";
var rateLimit = require("express-rate-limit");
module.exports = rateLimit({
windowMs: 60 * 1000,
max: 5,
standardHeaders: true,
legacyHeaders: false,
handler: function (req, res) {
return res.status(429).render("admin/login", {
error: "Quá nhiều lần thử đăng nhập, vui lòng thử lại sau ít phút.",
});
},
});
......@@ -4,10 +4,11 @@ module.exports = function (app) {
var adminAuth = require("../../app/controllers/adminAuth.server.controller");
var requireLogin = require("../middlewares/requireLogin");
var requireAdmin = require("../middlewares/requireAdmin");
var loginRateLimit = require("../middlewares/loginRateLimit");
app.route("/admin/login")
.get(adminAuth.loginForm)
.post(adminAuth.login);
.post(loginRateLimit, adminAuth.login);
app.route("/admin/logout").post(requireLogin, adminAuth.logout);
......
......@@ -24,6 +24,7 @@
"dateformat": "^3.0.3",
"dotenv": "^17.4.2",
"express": "^4.14.0",
"express-rate-limit": "^6.11.2",
"express-session": "^1.13.0",
"glob": "^7.2.3",
"he": "^0.5.0",
......@@ -1850,6 +1851,18 @@
"node": ">=0.8.0"
}
},
"node_modules/express-rate-limit": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.11.2.tgz",
"integrity": "sha512-a7uwwfNTh1U60ssiIkuLFWHt4hAC5yxlLGU2VP0X4YNlyEDZAqF4tK3GD3NSitVBrCQmQ0++0uOyFOgC2y4DDw==",
"license": "MIT",
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"express": "^4 || ^5"
}
},
"node_modules/express-session": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.0.tgz",
......
......@@ -29,6 +29,7 @@
"dateformat": "^3.0.3",
"dotenv": "^17.4.2",
"express": "^4.14.0",
"express-rate-limit": "^6.11.2",
"express-session": "^1.13.0",
"glob": "^7.2.3",
"he": "^0.5.0",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment