Add security headers via helmet and improve rate limiting
Add helmet middleware for security headers (CSP, X-Content-Type-Options, X-Frame-Options, HSTS, Referrer-Policy) and disable X-Powered-By. Add a global API rate limiter (100 req/min/IP) using express-rate-limit. Replace the hand-rolled in-memory login rate limiter (~25 lines) with a dedicated express-rate-limit instance (5 attempts/min/IP) on the login route. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cc2c651dfe
commit
a4ef21d099
40
package-lock.json
generated
40
package-lock.json
generated
@ -10,7 +10,9 @@
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1"
|
||||
"express": "^4.21.1",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"helmet": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.0"
|
||||
@ -594,6 +596,24 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
@ -750,6 +770,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@ -814,6 +843,15 @@
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"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",
|
||||
|
||||
@ -13,7 +13,9 @@
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1"
|
||||
"express": "^4.21.1",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"helmet": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.0"
|
||||
|
||||
@ -2,6 +2,8 @@ require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }
|
||||
const express = require('express');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const authRouter = require('./routes/auth');
|
||||
const { authMiddleware } = require('./middleware/auth');
|
||||
@ -16,6 +18,8 @@ const PORT = process.env.PORT || 3002;
|
||||
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
app.use(helmet());
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const cors = require('cors');
|
||||
app.use(cors({ origin: 'http://localhost:5173', credentials: true }));
|
||||
@ -24,6 +28,14 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 100,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
app.use('/api', apiLimiter);
|
||||
|
||||
app.use('/api/auth', authRouter);
|
||||
app.use('/api', authMiddleware);
|
||||
app.use('/api/products', productsRouter);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const router = express.Router();
|
||||
const {
|
||||
COOKIE_NAME,
|
||||
@ -8,43 +9,20 @@ const {
|
||||
clearSessionCookie,
|
||||
} = require('../middleware/auth');
|
||||
|
||||
// In-memory rate limiter for login attempts
|
||||
const loginAttempts = new Map();
|
||||
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute
|
||||
const RATE_LIMIT_MAX = 5;
|
||||
const loginLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 5,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Too many login attempts. Try again later.' },
|
||||
});
|
||||
|
||||
function isRateLimited(ip) {
|
||||
const now = Date.now();
|
||||
const attempts = loginAttempts.get(ip);
|
||||
if (!attempts) return false;
|
||||
// Remove expired entries
|
||||
const recent = attempts.filter(t => now - t < RATE_LIMIT_WINDOW_MS);
|
||||
if (recent.length === 0) {
|
||||
loginAttempts.delete(ip);
|
||||
return false;
|
||||
}
|
||||
loginAttempts.set(ip, recent);
|
||||
return recent.length >= RATE_LIMIT_MAX;
|
||||
}
|
||||
|
||||
function recordAttempt(ip) {
|
||||
const now = Date.now();
|
||||
const attempts = loginAttempts.get(ip) || [];
|
||||
attempts.push(now);
|
||||
loginAttempts.set(ip, attempts);
|
||||
}
|
||||
|
||||
router.post('/login', (req, res) => {
|
||||
router.post('/login', loginLimiter, (req, res) => {
|
||||
const password = process.env.APP_PASSWORD;
|
||||
if (!password) {
|
||||
return res.status(200).json({ ok: true });
|
||||
}
|
||||
|
||||
const ip = req.ip;
|
||||
if (isRateLimited(ip)) {
|
||||
return res.status(429).json({ error: 'Too many login attempts. Try again later.' });
|
||||
}
|
||||
|
||||
const submitted = req.body?.password || '';
|
||||
const submittedBuf = Buffer.from(String(submitted));
|
||||
const passwordBuf = Buffer.from(password);
|
||||
@ -55,7 +33,6 @@ router.post('/login', (req, res) => {
|
||||
}
|
||||
|
||||
if (!match) {
|
||||
recordAttempt(ip);
|
||||
return res.status(401).json({ error: 'Invalid password' });
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user