From 061977126ae1706f2629cff4091eb7b0c61da231 Mon Sep 17 00:00:00 2001 From: TheFlow Date: Fri, 24 Oct 2025 16:42:56 +1300 Subject: [PATCH] fix(csrf): enable newsletter subscription from mobile CRITICAL FIX: Newsletter subscription was returning "Forbidden" error because the CSRF protection was incorrectly configured. Root cause: - CSRF cookie was set with httpOnly: true - JavaScript cannot read httpOnly cookies - Frontend couldn't extract token to send in X-CSRF-Token header - Double-submit CSRF pattern requires client to read the cookie Changes: - csrf-protection.middleware.js: Set httpOnly: false (required for double-submit pattern) - blog.js: Extract CSRF token from cookie and include in X-CSRF-Token header Security Note: This is the correct implementation per OWASP guidelines for double-submit cookie CSRF protection. The cookie is still protected by SameSite: strict and domain restrictions. Fixes: #newsletter-subscription-forbidden-mobile --- .claude/session-state.json | 20 ++++++++++---------- public/js/blog.js | 9 ++++++++- src/middleware/csrf-protection.middleware.js | 2 +- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.claude/session-state.json b/.claude/session-state.json index 9aa992c2..b94acaa2 100644 --- a/.claude/session-state.json +++ b/.claude/session-state.json @@ -43,8 +43,8 @@ "last_deliberation": null }, "FileEditHook": { - "timestamp": "2025-10-24T03:32:50.799Z", - "file": "/home/theflow/projects/tractatus/public/js/admin/submission-modal-enhanced.js", + "timestamp": "2025-10-24T03:42:14.478Z", + "file": "/home/theflow/projects/tractatus/src/middleware/csrf-protection.middleware.js", "result": "passed" }, "FileWriteHook": { @@ -58,25 +58,25 @@ "tokens": 30000 }, "alerts": [], - "last_updated": "2025-10-24T03:32:50.799Z", + "last_updated": "2025-10-24T03:42:14.478Z", "initialized": true, "framework_components": { "CrossReferenceValidator": { "message": 0, "tokens": 0, - "timestamp": "2025-10-24T03:35:34.228Z", - "last_validation": "2025-10-24T03:35:34.228Z", - "validations_performed": 951 + "timestamp": "2025-10-24T03:42:14.476Z", + "last_validation": "2025-10-24T03:42:14.476Z", + "validations_performed": 957 }, "BashCommandValidator": { "message": 0, "tokens": 0, "timestamp": null, - "last_validation": "2025-10-24T03:35:34.229Z", - "validations_performed": 584, - "blocks_issued": 62 + "last_validation": "2025-10-24T03:42:26.291Z", + "validations_performed": 589, + "blocks_issued": 66 } }, - "action_count": 584, + "action_count": 589, "auto_compact_events": [] } \ No newline at end of file diff --git a/public/js/blog.js b/public/js/blog.js index ca82d788..ad3e830c 100644 --- a/public/js/blog.js +++ b/public/js/blog.js @@ -571,10 +571,17 @@ function setupNewsletterModal() { submitBtn.textContent = 'Subscribing...'; try { + // Get CSRF token from cookie + const csrfToken = document.cookie + .split('; ') + .find(row => row.startsWith('csrf-token=')) + ?.split('=')[1]; + const response = await fetch('/api/newsletter/subscribe', { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken || '' }, body: JSON.stringify({ email, diff --git a/src/middleware/csrf-protection.middleware.js b/src/middleware/csrf-protection.middleware.js index 4da885a1..2a9a9e12 100644 --- a/src/middleware/csrf-protection.middleware.js +++ b/src/middleware/csrf-protection.middleware.js @@ -79,7 +79,7 @@ function setCsrfToken(req, res, next) { const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https'; res.cookie('csrf-token', token, { - httpOnly: true, + httpOnly: false, // Must be false for double-submit pattern (client needs to read it) secure: isSecure && process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 24 * 60 * 60 * 1000 // 24 hours