From a27bd118e535563e7f053b36b8c71ee3cfb8df1f Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sat, 6 Jun 2026 22:14:53 +0530 Subject: [PATCH] fix: Cross-User Write, strict CORS, rate limiting - IDOR in sync api - if server was run in prod without jwt secret var then it fell back to inscure string; added startup check - restrict query requests to vite origin - use `express-rate-limit`. 100 requests per 15-minute window for a client --- server/index.js | 29 ++++++++++++++++++++++++----- server/package-lock.json | 28 ++++++++++++++++++++++++++++ server/package.json | 1 + 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/server/index.js b/server/index.js index c740cf4..f2ded22 100644 --- a/server/index.js +++ b/server/index.js @@ -12,12 +12,29 @@ const jwt = require('jsonwebtoken'); const pool = require('./db'); require('dotenv').config(); +if (process.env.NODE_ENV === 'production' && (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'your_jwt_secret_here')) { + console.error("FATAL: JWT_SECRET environment variable is not securely set in production!"); + process.exit(1); +} + +const rateLimit = require('express-rate-limit'); + const app = express(); const PORT = process.env.PORT || 5000; -app.use(cors()); +const allowedOrigin = process.env.ALLOWED_ORIGIN || 'http://localhost:5173'; +app.use(cors({ + origin: allowedOrigin +})); app.use(express.json()); +// Rate limiter for authentication routes +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // Limit each IP to 100 requests per window + message: { error: 'Too many authentication attempts. Please try again later.' } +}); + // Request logger middleware app.use((req, res, next) => { console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); @@ -44,7 +61,7 @@ const authenticateToken = (req, res, next) => { }; // Registration -app.post('/api/register', async (req, res) => { +app.post('/api/register', authLimiter, async (req, res) => { try { const { username, email, password } = req.body; @@ -76,7 +93,7 @@ app.post('/api/register', async (req, res) => { }); // Login -app.post('/api/login', async (req, res) => { +app.post('/api/login', authLimiter, async (req, res) => { try { const { email, password } = req.body; @@ -156,7 +173,8 @@ app.post('/api/sync', authenticateToken, async (req, res) => { image = EXCLUDED.image, updated_at = EXCLUDED.updated_at, is_deleted = EXCLUDED.is_deleted - WHERE EXCLUDED.updated_at > beans.updated_at OR beans.user_id IS NULL + WHERE (EXCLUDED.updated_at > beans.updated_at OR beans.user_id IS NULL) + AND (beans.user_id = EXCLUDED.user_id OR beans.user_id IS NULL) `, [ bean.id, userId, @@ -189,7 +207,8 @@ app.post('/api/sync', authenticateToken, async (req, res) => { created_at = EXCLUDED.created_at, updated_at = EXCLUDED.updated_at, is_deleted = EXCLUDED.is_deleted - WHERE EXCLUDED.updated_at > brew_logs.updated_at OR brew_logs.user_id IS NULL + WHERE (EXCLUDED.updated_at > brew_logs.updated_at OR brew_logs.user_id IS NULL) + AND (brew_logs.user_id = EXCLUDED.user_id OR brew_logs.user_id IS NULL) `, [ log.id, userId, diff --git a/server/package-lock.json b/server/package-lock.json index 0a950b0..4494287 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -13,6 +13,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "express-rate-limit": "^8.5.2", "jsonwebtoken": "^9.0.3", "pg": "^8.21.0" } @@ -333,6 +334,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/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -496,6 +515,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/server/package.json b/server/package.json index 7701909..559cee4 100644 --- a/server/package.json +++ b/server/package.json @@ -16,6 +16,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "express-rate-limit": "^8.5.2", "jsonwebtoken": "^9.0.3", "pg": "^8.21.0" }