tractatus/src/server.js
TheFlow 059dd43b72 security: complete Phase 0 Quick Wins implementation
Phase 0 Complete (QW-1 through QW-8):
 Enhanced input validation with HTML sanitization
 Form rate limiting (5 req/min on all submission endpoints)
 Modern CSRF protection (SameSite cookies + double-submit pattern)
 Security audit logging (CSRF violations captured)
 Applied to all public form endpoints:
   - /api/cases/submit (case studies)
   - /api/media/inquiries (media inquiries)
   - /api/newsletter/subscribe (newsletter)

New Middleware:
- csrf-protection.middleware.js (replaces deprecated csurf package)
- Enhanced input-validation.middleware.js applied to all forms

Security Features Active:
- Security headers (CSP, HSTS, X-Frame-Options, etc.)
- Rate limiting (100 req/15min public, 5 req/min forms)
- CSRF protection (double-submit cookie pattern)
- HTML sanitization (XSS prevention)
- Response sanitization (hide stack traces)
- Security event logging

Implements: inst_041, inst_042, inst_043, inst_044, inst_045, inst_046
Refs: docs/plans/security-implementation-roadmap.md Phase 0
2025-10-14 15:32:54 +13:00

267 lines
8.7 KiB
JavaScript

/**
* Tractatus Express Server
* Main application entry point
*/
require('dotenv').config();
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');
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');
const { setCsrfToken, csrfProtection, getCsrfToken } = require('./middleware/csrf-protection.middleware');
// Create Express app
const app = express();
// Trust proxy (for rate limiting behind reverse proxy)
app.set('trust proxy', 1);
// ============================================================
// 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: false, // Disabled - using our custom CSP in securityHeadersMiddleware
}));
// CORS
app.use(cors(config.cors));
// Cookie parser (required for CSRF)
app.use(cookieParser());
// Set CSRF token cookie on all requests
app.use(setCsrfToken);
// 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 (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);
// CSRF Protection (Modern Implementation - Phase 0 Complete)
// Uses SameSite cookies + double-submit cookie pattern
// Protection is applied selectively to state-changing routes (POST, PUT, DELETE, PATCH)
// Webhooks and public endpoints are excluded
// 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) => {
const path = req.path;
// Version manifest and service worker: No cache (always fetch fresh)
if (path === '/version.json' || path === '/service-worker.js') {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
}
// HTML files: Short cache, always revalidate
else if (path.endsWith('.html') || path === '/') {
res.setHeader('Cache-Control', 'public, max-age=300, must-revalidate'); // 5 minutes
}
// CSS and JS files: Longer cache (we use version parameters)
else if (path.endsWith('.css') || path.endsWith('.js')) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); // 1 year
}
// Images and fonts: Long cache
else if (path.match(/\.(jpg|jpeg|png|gif|svg|ico|woff|woff2|ttf|eot)$/)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); // 1 year
}
// PWA manifest: Medium cache
else if (path === '/manifest.json') {
res.setHeader('Cache-Control', 'public, max-age=86400'); // 1 day
}
// Everything else: Short cache
else {
res.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour
}
next();
});
// Static files
app.use(express.static('public'));
// Health check endpoint (minimal, no sensitive data)
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString()
});
});
// CSRF token endpoint (modern implementation)
// Returns the CSRF token from cookie for client-side usage
app.get('/api/csrf-token', getCsrfToken);
// API routes
const apiRoutes = require('./routes/index');
app.use('/api', apiRoutes);
// Homepage (temporary)
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Tractatus AI Safety Framework</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
line-height: 1.6;
}
h1 { color: #2563eb; }
.status { color: #059669; font-weight: bold; }
code { background: #f3f4f6; padding: 2px 6px; border-radius: 3px; }
</style>
</head>
<body>
<h1>Tractatus AI Safety Framework</h1>
<p class="status">✓ Server Running</p>
<p>Development environment for the Tractatus-Based LLM Safety Framework website.</p>
<h2>Status</h2>
<ul>
<li>✓ MongoDB connected (port 27017)</li>
<li>✓ Express server running (port ${config.port})</li>
<li>✓ Database initialized (10 collections)</li>
<li>✓ Core models implemented</li>
<li>✓ API routes complete (auth, documents, blog, admin)</li>
<li>✓ Governance services active (6 core services)</li>
<li>⏳ Frontend (pending)</li>
</ul>
<h2>Available Endpoints</h2>
<ul>
<li><code>GET /health</code> - Health check</li>
<li><code>GET /api</code> - API documentation</li>
<li><code>POST /api/auth/login</code> - Admin login</li>
<li><code>GET /api/documents</code> - List framework documents</li>
<li><code>GET /api/blog</code> - List published blog posts</li>
<li><code>GET /api/admin/stats</code> - System statistics (auth required)</li>
</ul>
<p><em>Phase 1 Development - Not for public use</em></p>
</body>
</html>
`);
});
// ============================================================
// ERROR HANDLING (Quick Wins)
// ============================================================
// 404 handler
app.use(notFound);
// Enhanced error handler (sanitizes responses, hides stack traces)
app.use(sanitizeErrorResponse);
// Fallback to original error handler if needed
app.use(errorHandler);
// Server startup
async function start() {
try {
// Connect to MongoDB (native driver)
await connectDb();
// Connect Mongoose (for ODM models)
await connectMongoose();
// Initialize governance services
const BoundaryEnforcer = require('./services/BoundaryEnforcer.service');
await BoundaryEnforcer.initialize();
const PluralisticDeliberationOrchestrator = require('./services/PluralisticDeliberationOrchestrator.service');
await PluralisticDeliberationOrchestrator.initialize();
logger.info('✅ Governance services initialized (6 core services)');
// Start server
const server = app.listen(config.port, () => {
logger.info(`🚀 Tractatus server started`);
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`);
});
// Graceful shutdown
process.on('SIGTERM', () => shutdown(server));
process.on('SIGINT', () => shutdown(server));
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
// Graceful shutdown
async function shutdown(server) {
logger.info('Shutting down gracefully...');
server.close(async () => {
logger.info('HTTP server closed');
await closeDb();
logger.info('Native MongoDB connection closed');
await closeMongoose();
logger.info('Mongoose connection closed');
process.exit(0);
});
// Force shutdown after 10 seconds
setTimeout(() => {
logger.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
}
// Start server if run directly
if (require.main === module) {
start();
}
module.exports = app;