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
This commit is contained in:
parent
d5af9a1a6b
commit
b078eec634
4 changed files with 233 additions and 29 deletions
152
package-lock.json
generated
152
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
101
src/server.js
101
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`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue