diff --git a/PERPLEXITY_TECHNICAL_BRIEF_FAQ_SCROLLBAR.md b/PERPLEXITY_TECHNICAL_BRIEF_FAQ_SCROLLBAR.md
new file mode 100644
index 00000000..afa3a417
--- /dev/null
+++ b/PERPLEXITY_TECHNICAL_BRIEF_FAQ_SCROLLBAR.md
@@ -0,0 +1,201 @@
+# Technical Brief: FAQ Modal Scrollbar Issue
+
+**Date**: 2025-10-14
+**Project**: Tractatus AI Safety Framework
+**URL**: https://agenticgovernance.digital/faq.html
+**Requesting AI**: Claude Code (Sonnet 4.5)
+**Target AI**: Perplexity or other web-capable AI assistant
+
+---
+
+## PROBLEM STATEMENT
+
+User reports FAQ modal scrollbar is not visible/functional. Modal shows only ~8 FAQ questions when 28+ exist in the DOM. User cannot access remaining content.
+
+**User quote**: "there is no scroll slider showing in production"
+
+---
+
+## CURRENT STATE (2025-10-14 00:30 UTC)
+
+### Modal Structure
+```html
+
+
-
Most Common Questions
+
Featured Questions
diff --git a/src/middleware/csrf-protection.middleware.js b/src/middleware/csrf-protection.middleware.js
new file mode 100644
index 00000000..53cc2a55
--- /dev/null
+++ b/src/middleware/csrf-protection.middleware.js
@@ -0,0 +1,115 @@
+/**
+ * CSRF Protection Middleware (Modern Approach)
+ *
+ * Uses SameSite cookies + double-submit cookie pattern
+ * Replaces deprecated csurf package
+ *
+ * Reference: OWASP CSRF Prevention Cheat Sheet
+ * https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
+ */
+
+const crypto = require('crypto');
+const { logSecurityEvent, getClientIp } = require('../utils/security-logger');
+
+/**
+ * Generate CSRF token
+ */
+function generateCsrfToken() {
+ return crypto.randomBytes(32).toString('hex');
+}
+
+/**
+ * CSRF Protection Middleware
+ *
+ * Uses double-submit cookie pattern:
+ * 1. Server sets CSRF token in secure, SameSite cookie
+ * 2. Client must send same token in custom header (X-CSRF-Token)
+ * 3. Server validates cookie matches header
+ */
+function csrfProtection(req, res, next) {
+ // Skip GET, HEAD, OPTIONS (safe methods)
+ if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
+ return next();
+ }
+
+ // Get CSRF token from cookie
+ const cookieToken = req.cookies['csrf-token'];
+
+ // Get CSRF token from header
+ const headerToken = req.headers['x-csrf-token'] || req.headers['csrf-token'];
+
+ // Validate tokens exist and match
+ if (!cookieToken || !headerToken || cookieToken !== headerToken) {
+ logSecurityEvent({
+ type: 'csrf_violation',
+ sourceIp: getClientIp(req),
+ userId: req.user?.id,
+ endpoint: req.path,
+ userAgent: req.get('user-agent'),
+ details: {
+ method: req.method,
+ hasCookie: !!cookieToken,
+ hasHeader: !!headerToken,
+ tokensMatch: cookieToken === headerToken
+ },
+ action: 'blocked',
+ severity: 'high'
+ });
+
+ return res.status(403).json({
+ error: 'Forbidden',
+ message: 'Invalid CSRF token',
+ code: 'CSRF_VALIDATION_FAILED'
+ });
+ }
+
+ next();
+}
+
+/**
+ * Middleware to set CSRF token cookie
+ * Apply this globally or on routes that need CSRF protection
+ */
+function setCsrfToken(req, res, next) {
+ // Only set cookie if it doesn't exist
+ if (!req.cookies['csrf-token']) {
+ const token = generateCsrfToken();
+
+ res.cookie('csrf-token', token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'strict',
+ maxAge: 24 * 60 * 60 * 1000 // 24 hours
+ });
+ }
+
+ next();
+}
+
+/**
+ * Endpoint to get CSRF token for client-side usage
+ * GET /api/csrf-token
+ *
+ * Returns the CSRF token from the cookie so client can include it in requests
+ */
+function getCsrfToken(req, res) {
+ const token = req.cookies['csrf-token'];
+
+ if (!token) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'No CSRF token found. Visit the site first to receive a token.'
+ });
+ }
+
+ res.json({
+ csrfToken: token
+ });
+}
+
+module.exports = {
+ csrfProtection,
+ setCsrfToken,
+ getCsrfToken,
+ generateCsrfToken
+};
diff --git a/src/routes/cases.routes.js b/src/routes/cases.routes.js
index b5bcca37..f56b4654 100644
--- a/src/routes/cases.routes.js
+++ b/src/routes/cases.routes.js
@@ -10,13 +10,32 @@ const casesController = require('../controllers/cases.controller');
const { authenticateToken, requireRole } = require('../middleware/auth.middleware');
const { validateRequired, validateEmail, validateObjectId } = require('../middleware/validation.middleware');
const { asyncHandler } = require('../middleware/error.middleware');
+const { createInputValidationMiddleware } = require('../middleware/input-validation.middleware');
+const { formRateLimiter } = require('../middleware/rate-limit.middleware');
+const { csrfProtection } = require('../middleware/csrf-protection.middleware');
/**
* Public routes
*/
+// Validation schema for case study submission
+const caseSubmissionSchema = {
+ 'submitter.name': { required: true, type: 'name', maxLength: 100 },
+ 'submitter.email': { required: true, type: 'email', maxLength: 254 },
+ 'submitter.organization': { required: false, type: 'default', maxLength: 200 },
+ 'case_study.title': { required: true, type: 'title', maxLength: 200 },
+ 'case_study.description': { required: true, type: 'description', maxLength: 50000 },
+ 'case_study.failure_mode': { required: true, type: 'default', maxLength: 500 },
+ 'case_study.context': { required: false, type: 'default', maxLength: 5000 },
+ 'case_study.impact': { required: false, type: 'default', maxLength: 5000 },
+ 'case_study.lessons_learned': { required: false, type: 'default', maxLength: 5000 }
+};
+
// POST /api/cases/submit - Submit case study (public)
router.post('/submit',
+ formRateLimiter, // 5 requests per minute
+ csrfProtection, // CSRF validation
+ createInputValidationMiddleware(caseSubmissionSchema),
validateRequired([
'submitter.name',
'submitter.email',
diff --git a/src/routes/media.routes.js b/src/routes/media.routes.js
index 31dd6cab..4e66ad49 100644
--- a/src/routes/media.routes.js
+++ b/src/routes/media.routes.js
@@ -10,13 +10,31 @@ const mediaController = require('../controllers/media.controller');
const { authenticateToken, requireRole } = require('../middleware/auth.middleware');
const { validateRequired, validateEmail, validateObjectId } = require('../middleware/validation.middleware');
const { asyncHandler } = require('../middleware/error.middleware');
+const { createInputValidationMiddleware } = require('../middleware/input-validation.middleware');
+const { formRateLimiter } = require('../middleware/rate-limit.middleware');
+const { csrfProtection } = require('../middleware/csrf-protection.middleware');
/**
* Public routes
*/
+// Validation schema for media inquiry submission
+const mediaInquirySchema = {
+ 'contact.name': { required: true, type: 'name', maxLength: 100 },
+ 'contact.email': { required: true, type: 'email', maxLength: 254 },
+ 'contact.outlet': { required: true, type: 'default', maxLength: 200 },
+ 'contact.phone': { required: false, type: 'phone', maxLength: 20 },
+ 'contact.role': { required: false, type: 'default', maxLength: 100 },
+ 'inquiry.subject': { required: true, type: 'title', maxLength: 200 },
+ 'inquiry.message': { required: true, type: 'description', maxLength: 5000 },
+ 'inquiry.deadline': { required: false, type: 'default', maxLength: 100 }
+};
+
// POST /api/media/inquiries - Submit media inquiry (public)
router.post('/inquiries',
+ formRateLimiter, // 5 requests per minute
+ csrfProtection, // CSRF validation
+ createInputValidationMiddleware(mediaInquirySchema),
validateRequired(['contact.name', 'contact.email', 'contact.outlet', 'inquiry.subject', 'inquiry.message']),
validateEmail('contact.email'),
asyncHandler(mediaController.submitInquiry)
diff --git a/src/routes/newsletter.routes.js b/src/routes/newsletter.routes.js
index c8420b18..f7557d13 100644
--- a/src/routes/newsletter.routes.js
+++ b/src/routes/newsletter.routes.js
@@ -10,13 +10,25 @@ const newsletterController = require('../controllers/newsletter.controller');
const { authenticateToken, requireRole } = require('../middleware/auth.middleware');
const { validateRequired } = require('../middleware/validation.middleware');
const { asyncHandler } = require('../middleware/error.middleware');
+const { createInputValidationMiddleware } = require('../middleware/input-validation.middleware');
+const { formRateLimiter } = require('../middleware/rate-limit.middleware');
+const { csrfProtection } = require('../middleware/csrf-protection.middleware');
/**
* Public Routes
*/
+// Validation schema for newsletter subscription
+const newsletterSubscribeSchema = {
+ 'email': { required: true, type: 'email', maxLength: 254 },
+ 'name': { required: false, type: 'name', maxLength: 100 }
+};
+
// POST /api/newsletter/subscribe - Subscribe to newsletter
router.post('/subscribe',
+ formRateLimiter, // 5 requests per minute
+ csrfProtection, // CSRF validation
+ createInputValidationMiddleware(newsletterSubscribeSchema),
validateRequired(['email']),
asyncHandler(newsletterController.subscribe)
);
diff --git a/src/server.js b/src/server.js
index 82f77a87..e0ecbdae 100644
--- a/src/server.js
+++ b/src/server.js
@@ -22,6 +22,7 @@ const { notFound, errorHandler } = require('./middleware/error.middleware');
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();
@@ -47,6 +48,9 @@ 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);
@@ -63,18 +67,10 @@ app.use(express.urlencoded({ extended: true, limit: '1mb' }));
// Request logging
app.use(logger.request);
-// 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();
-// });
+// 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
@@ -125,10 +121,9 @@ 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() });
-// });
+// 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');
@@ -192,27 +187,6 @@ 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);