From b078eec6344db32bf4462b520083723a5aef8b96 Mon Sep 17 00:00:00 2001 From: TheFlow Date: Tue, 14 Oct 2025 15:18:49 +1300 Subject: [PATCH] security: implement Quick Wins security middleware (inst_041-046) - Add security headers middleware (CSP, HSTS, X-Frame-Options, etc.) - Add rate limiting (100 req/15min public, 5 req/min forms) - Add input validation and sanitization middleware - Add response sanitization (hide stack traces, remove sensitive fields) - Add centralized security event logging to audit trail - Disable CSRF (deprecated package, will implement modern solution in Phase 3) - Update security logger to use HOME-based log path Implements: inst_041, inst_042, inst_043, inst_044, inst_045, inst_046 Refs: docs/plans/security-implementation-roadmap.md --- package-lock.json | 152 ++++++++++++++++++++++++++++++++++- package.json | 6 +- src/server.js | 101 +++++++++++++++++------ src/utils/security-logger.js | 3 +- 4 files changed, 233 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 269a4eb8..f4fffc26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "license": "Apache-2.0", "dependencies": { "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "csurf": "^1.11.0", "dotenv": "^16.3.1", "express": "^4.18.2", - "express-rate-limit": "^7.1.5", + "express-rate-limit": "^7.5.1", "helmet": "^7.1.0", "highlight.js": "^11.9.0", "jsonwebtoken": "^9.0.2", @@ -23,7 +25,7 @@ "puppeteer": "^24.23.0", "sanitize-html": "^2.11.0", "stripe": "^14.25.0", - "validator": "^13.11.0", + "validator": "^13.15.15", "winston": "^3.11.0" }, "devDependencies": { @@ -2738,6 +2740,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -2827,6 +2851,20 @@ "node": ">= 8" } }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "license": "MIT", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2840,6 +2878,80 @@ "node": ">=4" } }, + "node_modules/csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "deprecated": "This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions", + "license": "MIT", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/csurf/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "license": "ISC" + }, + "node_modules/csurf/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -7165,6 +7277,15 @@ ], "license": "MIT" }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7332,6 +7453,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8419,6 +8546,15 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8474,6 +8610,18 @@ "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", "license": "MIT" }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/package.json b/package.json index ef831868..dfe99a5c 100644 --- a/package.json +++ b/package.json @@ -41,10 +41,12 @@ "license": "Apache-2.0", "dependencies": { "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "csurf": "^1.11.0", "dotenv": "^16.3.1", "express": "^4.18.2", - "express-rate-limit": "^7.1.5", + "express-rate-limit": "^7.5.1", "helmet": "^7.1.0", "highlight.js": "^11.9.0", "jsonwebtoken": "^9.0.2", @@ -54,7 +56,7 @@ "puppeteer": "^24.23.0", "sanitize-html": "^2.11.0", "stripe": "^14.25.0", - "validator": "^13.11.0", + "validator": "^13.15.15", "winston": "^3.11.0" }, "devDependencies": { diff --git a/src/server.js b/src/server.js index 976dba08..82f77a87 100644 --- a/src/server.js +++ b/src/server.js @@ -9,6 +9,8 @@ const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const rateLimit = require('express-rate-limit'); +const cookieParser = require('cookie-parser'); +// const csrf = require('csurf'); // Disabled - deprecated package, will implement modern solution in Phase 3 const config = require('./config/app.config'); const logger = require('./utils/logger.util'); @@ -16,51 +18,67 @@ const { connect: connectDb, close: closeDb } = require('./utils/db.util'); const { connect: connectMongoose, close: closeMongoose } = require('./utils/mongoose.util'); const { notFound, errorHandler } = require('./middleware/error.middleware'); +// Security middleware (Quick Wins) +const { securityHeadersMiddleware } = require('./middleware/security-headers.middleware'); +const { publicRateLimiter, formRateLimiter, authRateLimiter } = require('./middleware/rate-limit.middleware'); +const { sanitizeErrorResponse, sanitizeResponseData } = require('./middleware/response-sanitization.middleware'); + // Create Express app const app = express(); // Trust proxy (for rate limiting behind reverse proxy) app.set('trust proxy', 1); -// Security middleware +// ============================================================ +// SECURITY MIDDLEWARE (Quick Wins - inst_041-046) +// ============================================================ + +// Enhanced security headers (replaces helmet CSP with more specific policy) +app.use(securityHeadersMiddleware); + +// Keep helmet for other security features (but CSP already set above) app.use(helmet({ - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"], - scriptSrc: ["'self'", "https://cdnjs.cloudflare.com"], - connectSrc: ["'self'", "https://cdnjs.cloudflare.com"], - fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], - imgSrc: ["'self'", "data:", "https:"], - }, - }, + contentSecurityPolicy: false, // Disabled - using our custom CSP in securityHeadersMiddleware })); // CORS app.use(cors(config.cors)); +// Cookie parser (required for CSRF) +app.use(cookieParser()); + +// Response data sanitization (removes sensitive fields) +app.use(sanitizeResponseData); + // Raw body capture for Stripe webhooks (must be before JSON parser) app.use('/api/koha/webhook', express.raw({ type: 'application/json' }), (req, res, next) => { req.rawBody = req.body; next(); }); -// Body parsers -app.use(express.json({ limit: '10mb' })); -app.use(express.urlencoded({ extended: true, limit: '10mb' })); +// Body parsers (reduced limit from 10mb to 1mb for security) +app.use(express.json({ limit: '1mb' })); +app.use(express.urlencoded({ extended: true, limit: '1mb' })); // Request logging app.use(logger.request); -// Rate limiting -const limiter = rateLimit({ - windowMs: config.security.rateLimitWindowMs, - max: config.security.rateLimitMaxRequests, - message: 'Too many requests from this IP, please try again later', - standardHeaders: true, - legacyHeaders: false, -}); -app.use('/api/', limiter); +// CSRF Protection - Disabled (deprecated package) +// TODO Phase 3: Implement modern CSRF solution (e.g., double-submit cookie pattern) +// const csrfProtection = csrf({ cookie: true }); +// app.use((req, res, next) => { +// if (req.path === '/api/koha/webhook') { +// return next(); +// } +// if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) { +// return csrfProtection(req, res, next); +// } +// next(); +// }); + +// Enhanced rate limiting (Quick Wins) +// Public endpoints: 100 requests per 15 minutes per IP +app.use(publicRateLimiter); // Cache control middleware for static assets app.use((req, res, next) => { @@ -107,6 +125,11 @@ app.get('/health', (req, res) => { }); }); +// CSRF token endpoint - Disabled (will implement in Phase 3 with modern solution) +// app.get('/api/csrf-token', csrfProtection, (req, res) => { +// res.json({ csrfToken: req.csrfToken() }); +// }); + // API routes const apiRoutes = require('./routes/index'); app.use('/api', apiRoutes); @@ -165,10 +188,38 @@ app.get('/', (req, res) => { `); }); +// ============================================================ +// ERROR HANDLING (Quick Wins) +// ============================================================ + +// CSRF Error Handler - Disabled (will implement in Phase 3) +// app.use((err, req, res, next) => { +// if (err.code === 'EBADCSRFTOKEN') { +// logSecurityEvent({ +// type: 'csrf_violation', +// sourceIp: getClientIp(req), +// userId: req.user?.id, +// endpoint: req.path, +// userAgent: req.get('user-agent'), +// details: { method: req.method }, +// action: 'blocked', +// severity: 'high' +// }); +// return res.status(403).json({ +// error: 'Invalid CSRF token', +// message: 'Request blocked for security reasons' +// }); +// } +// next(err); +// }); + // 404 handler app.use(notFound); -// Error handler +// Enhanced error handler (sanitizes responses, hides stack traces) +app.use(sanitizeErrorResponse); + +// Fallback to original error handler if needed app.use(errorHandler); // Server startup @@ -195,6 +246,8 @@ async function start() { logger.info(`āœ… Environment: ${config.env}`); logger.info(`āœ… Port: ${config.port}`); logger.info(`āœ… MongoDB: ${config.mongodb.db}`); + logger.info(`šŸ”’ Security: Quick Wins active (headers, rate limiting, input validation)`); + logger.info(`šŸ“Š Security logs: ${process.env.HOME}/var/log/tractatus/security-audit.log`); logger.info(`✨ Ready for development`); console.log(`\n🌐 http://localhost:${config.port}\n`); }); diff --git a/src/utils/security-logger.js b/src/utils/security-logger.js index dd4f2a04..ef38955e 100644 --- a/src/utils/security-logger.js +++ b/src/utils/security-logger.js @@ -9,7 +9,8 @@ const fs = require('fs').promises; const path = require('path'); -const SECURITY_LOG_PATH = '/var/log/tractatus/security-audit.log'; +const SECURITY_LOG_PATH = process.env.SECURITY_LOG_PATH || + (process.env.HOME ? `${process.env.HOME}/var/log/tractatus/security-audit.log` : '/var/log/tractatus/security-audit.log'); /** * Log a security event to audit trail