From 3a20d5cc089d84aabd0942fb4114bd1b45f9f9e7 Mon Sep 17 00:00:00 2001 From: Sergei Date: Fri, 19 Dec 2025 09:50:27 -0800 Subject: [PATCH] Add security middleware to backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security features: - Helmet: Security headers (XSS, clickjacking protection) - CORS: Whitelist only allowed domains - Rate Limiting: 100 req/15min general, 5 req/15min for auth - Stripe webhook signature verification (already had) - Admin API key protection (already had) Allowed origins: - wellnuo.smartlaunchhub.com - wellnuo.com - localhost (dev) - Expo dev URLs πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/package-lock.json | 38 +++++++++++++++++++++++++ backend/package.json | 2 ++ backend/src/index.js | 60 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index dbbea24..3df3137 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,8 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-rate-limit": "^8.2.1", + "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "pg": "^8.16.3", @@ -571,6 +573,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/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -740,6 +760,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", @@ -794,6 +823,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "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", diff --git a/backend/package.json b/backend/package.json index 32e709c..bcf5008 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,8 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-rate-limit": "^8.2.1", + "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "pg": "^8.16.3", diff --git a/backend/src/index.js b/backend/src/index.js index 5c8fa1b..53f1df5 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,6 +1,8 @@ require('dotenv').config(); const express = require('express'); const cors = require('cors'); +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); const path = require('path'); const apiRouter = require('./routes/api'); const stripeRouter = require('./routes/stripe'); @@ -10,8 +12,62 @@ const adminRouter = require('./routes/admin'); const app = express(); const PORT = process.env.PORT || 3000; -// CORS -app.use(cors()); +// ============ SECURITY ============ + +// Helmet - добавляСт security headers +app.use(helmet({ + contentSecurityPolicy: false // ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌ для admin panel +})); + +// CORS - Ρ€Π°Π·Ρ€Π΅ΡˆΠ°Π΅ΠΌ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ наши Π΄ΠΎΠΌΠ΅Π½Ρ‹ +const allowedOrigins = [ + 'https://wellnuo.smartlaunchhub.com', + 'https://wellnuo.com', + 'http://localhost:3000', + 'http://localhost:8081', // Expo dev + 'exp://192.168.1.*' // Expo local +]; + +app.use(cors({ + origin: function(origin, callback) { + // Π Π°Π·Ρ€Π΅ΡˆΠ°Π΅ΠΌ запросы Π±Π΅Π· origin (mobile apps, Postman) + if (!origin) return callback(null, true); + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Ρ€Π°Π·Ρ€Π΅ΡˆΡ‘Π½Π½Ρ‹Π΅ Π΄ΠΎΠΌΠ΅Π½Ρ‹ + if (allowedOrigins.some(allowed => { + if (allowed.includes('*')) { + const regex = new RegExp(allowed.replace('*', '.*')); + return regex.test(origin); + } + return allowed === origin; + })) { + return callback(null, true); + } + + callback(new Error('Not allowed by CORS')); + }, + credentials: true +})); + +// Rate Limiting - Π·Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ DDoS +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 ΠΌΠΈΠ½ΡƒΡ‚ + max: 100, // максимум 100 запросов Π·Π° 15 ΠΌΠΈΠ½ΡƒΡ‚ + message: { error: 'Too many requests, please try again later' }, + standardHeaders: true, + legacyHeaders: false +}); + +// Π‘ΠΎΠ»Π΅Π΅ строгий Π»ΠΈΠΌΠΈΡ‚ для auth endpoints +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5, // Ρ‚ΠΎΠ»ΡŒΠΊΠΎ 5 ΠΏΠΎΠΏΡ‹Ρ‚ΠΎΠΊ Π»ΠΎΠ³ΠΈΠ½Π° Π·Π° 15 ΠΌΠΈΠ½ΡƒΡ‚ + message: { error: 'Too many login attempts, please try again later' } +}); + +// ΠŸΡ€ΠΈΠΌΠ΅Π½ΡΠ΅ΠΌ rate limiting +app.use(limiter); +app.use('/function/well-api/api', authLimiter); // Legacy API с auth // Stripe webhooks need raw body for signature verification // Must be before express.json()