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
This commit is contained in:
TheFlow 2025-10-24 16:42:56 +13:00
parent 880d70d088
commit 061977126a
3 changed files with 19 additions and 12 deletions

View file

@ -43,8 +43,8 @@
"last_deliberation": null "last_deliberation": null
}, },
"FileEditHook": { "FileEditHook": {
"timestamp": "2025-10-24T03:32:50.799Z", "timestamp": "2025-10-24T03:42:14.478Z",
"file": "/home/theflow/projects/tractatus/public/js/admin/submission-modal-enhanced.js", "file": "/home/theflow/projects/tractatus/src/middleware/csrf-protection.middleware.js",
"result": "passed" "result": "passed"
}, },
"FileWriteHook": { "FileWriteHook": {
@ -58,25 +58,25 @@
"tokens": 30000 "tokens": 30000
}, },
"alerts": [], "alerts": [],
"last_updated": "2025-10-24T03:32:50.799Z", "last_updated": "2025-10-24T03:42:14.478Z",
"initialized": true, "initialized": true,
"framework_components": { "framework_components": {
"CrossReferenceValidator": { "CrossReferenceValidator": {
"message": 0, "message": 0,
"tokens": 0, "tokens": 0,
"timestamp": "2025-10-24T03:35:34.228Z", "timestamp": "2025-10-24T03:42:14.476Z",
"last_validation": "2025-10-24T03:35:34.228Z", "last_validation": "2025-10-24T03:42:14.476Z",
"validations_performed": 951 "validations_performed": 957
}, },
"BashCommandValidator": { "BashCommandValidator": {
"message": 0, "message": 0,
"tokens": 0, "tokens": 0,
"timestamp": null, "timestamp": null,
"last_validation": "2025-10-24T03:35:34.229Z", "last_validation": "2025-10-24T03:42:26.291Z",
"validations_performed": 584, "validations_performed": 589,
"blocks_issued": 62 "blocks_issued": 66
} }
}, },
"action_count": 584, "action_count": 589,
"auto_compact_events": [] "auto_compact_events": []
} }

View file

@ -571,10 +571,17 @@ function setupNewsletterModal() {
submitBtn.textContent = 'Subscribing...'; submitBtn.textContent = 'Subscribing...';
try { 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', { const response = await fetch('/api/newsletter/subscribe', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken || ''
}, },
body: JSON.stringify({ body: JSON.stringify({
email, email,

View file

@ -79,7 +79,7 @@ function setCsrfToken(req, res, next) {
const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https'; const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https';
res.cookie('csrf-token', token, { 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', secure: isSecure && process.env.NODE_ENV === 'production',
sameSite: 'strict', sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours maxAge: 24 * 60 * 60 * 1000 // 24 hours