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",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1"
|
"express": "^4.21.1",
|
||||||
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"helmet": "^8.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.0"
|
"concurrently": "^9.1.0"
|
||||||
@ -594,6 +596,24 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"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": ">= 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": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
@ -814,6 +843,15 @@
|
|||||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|||||||
@ -13,7 +13,9 @@
|
|||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1"
|
"express": "^4.21.1",
|
||||||
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"helmet": "^8.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.0"
|
"concurrently": "^9.1.0"
|
||||||
|
|||||||
@ -2,6 +2,8 @@ require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
|
||||||
const authRouter = require('./routes/auth');
|
const authRouter = require('./routes/auth');
|
||||||
const { authMiddleware } = require('./middleware/auth');
|
const { authMiddleware } = require('./middleware/auth');
|
||||||
@ -16,6 +18,8 @@ const PORT = process.env.PORT || 3002;
|
|||||||
|
|
||||||
app.set('trust proxy', 1);
|
app.set('trust proxy', 1);
|
||||||
|
|
||||||
|
app.use(helmet());
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
app.use(cors({ origin: 'http://localhost:5173', credentials: true }));
|
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(express.json());
|
||||||
app.use(cookieParser());
|
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/auth', authRouter);
|
||||||
app.use('/api', authMiddleware);
|
app.use('/api', authMiddleware);
|
||||||
app.use('/api/products', productsRouter);
|
app.use('/api/products', productsRouter);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const {
|
const {
|
||||||
COOKIE_NAME,
|
COOKIE_NAME,
|
||||||
@ -8,43 +9,20 @@ const {
|
|||||||
clearSessionCookie,
|
clearSessionCookie,
|
||||||
} = require('../middleware/auth');
|
} = require('../middleware/auth');
|
||||||
|
|
||||||
// In-memory rate limiter for login attempts
|
const loginLimiter = rateLimit({
|
||||||
const loginAttempts = new Map();
|
windowMs: 60 * 1000,
|
||||||
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute
|
max: 5,
|
||||||
const RATE_LIMIT_MAX = 5;
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
message: { error: 'Too many login attempts. Try again later.' },
|
||||||
|
});
|
||||||
|
|
||||||
function isRateLimited(ip) {
|
router.post('/login', loginLimiter, (req, res) => {
|
||||||
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) => {
|
|
||||||
const password = process.env.APP_PASSWORD;
|
const password = process.env.APP_PASSWORD;
|
||||||
if (!password) {
|
if (!password) {
|
||||||
return res.status(200).json({ ok: true });
|
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 submitted = req.body?.password || '';
|
||||||
const submittedBuf = Buffer.from(String(submitted));
|
const submittedBuf = Buffer.from(String(submitted));
|
||||||
const passwordBuf = Buffer.from(password);
|
const passwordBuf = Buffer.from(password);
|
||||||
@ -55,7 +33,6 @@ router.post('/login', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
recordAttempt(ip);
|
|
||||||
return res.status(401).json({ error: 'Invalid password' });
|
return res.status(401).json({ error: 'Invalid password' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user