From 6b79f9a1556c20f8959261bdc0f23d6caef261ac Mon Sep 17 00:00:00 2001 From: TheFlow Date: Sat, 25 Oct 2025 09:37:16 +1300 Subject: [PATCH] fix(newsletter): resolve CSRF token issue for static HTML pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: - nginx serves blog.html as static file, bypassing Express middleware - setCsrfToken middleware never runs - No CSRF cookie set - Newsletter subscription fails with 403 Forbidden Root cause: nginx config: 'try_files $uri @proxy' serves static files directly Location: /etc/nginx/sites-available/tractatus (line 54) Solution: 1. blog.js now fetches CSRF token via /api/csrf-token on page load 2. getCsrfToken endpoint now creates token if missing (for static pages) 3. Newsletter form uses fetched token for subscription Testing: ✅ Local test: CSRF token fetched successfully ✅ Newsletter subscription: Creates record in database ✅ Verified: test-fix@example.com subscribed via curl test Impact: - Newsletter subscriptions now work on production - Fix applies to all static HTML pages (blog.html, etc.) - Maintains CSRF protection security Files: - public/js/blog.js: Added fetchCsrfToken() + use in newsletter form - src/middleware/csrf-protection.middleware.js: Enhanced getCsrfToken() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- public/js/blog.js | 40 +++++++++++++++++--- src/middleware/csrf-protection.middleware.js | 19 +++++++--- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/public/js/blog.js b/public/js/blog.js index ad3e830c..67bba458 100644 --- a/public/js/blog.js +++ b/public/js/blog.js @@ -16,11 +16,17 @@ const activeFilters = { sort: 'date-desc' }; +// CSRF token (fetched on page load) +let csrfToken = null; + /** * Initialize the blog page */ async function init() { try { + // Fetch CSRF token first (required for newsletter subscription) + await fetchCsrfToken(); + await loadPosts(); attachEventListeners(); } catch (error) { @@ -29,6 +35,25 @@ async function init() { } } +/** + * Fetch CSRF token from server + * Required because nginx serves blog.html as static file (bypasses setCsrfToken middleware) + */ +async function fetchCsrfToken() { + try { + const response = await fetch('/api/csrf-token'); + const data = await response.json(); + + if (data.csrfToken) { + csrfToken = data.csrfToken; + console.log('CSRF token fetched successfully'); + } + } catch (error) { + console.warn('Failed to fetch CSRF token:', error); + // Non-critical - newsletter subscription will fail but blog browsing still works + } +} + /** * Load all published blog posts from API */ @@ -571,17 +596,20 @@ function setupNewsletterModal() { submitBtn.textContent = 'Subscribing...'; try { - // Get CSRF token from cookie - const csrfToken = document.cookie - .split('; ') - .find(row => row.startsWith('csrf-token=')) - ?.split('=')[1]; + // Ensure we have a CSRF token + if (!csrfToken) { + await fetchCsrfToken(); + } + + if (!csrfToken) { + throw new Error('Unable to obtain CSRF token'); + } const response = await fetch('/api/newsletter/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-CSRF-Token': csrfToken || '' + '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 2a9a9e12..475a845b 100644 --- a/src/middleware/csrf-protection.middleware.js +++ b/src/middleware/csrf-protection.middleware.js @@ -93,15 +93,24 @@ function setCsrfToken(req, res, 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 + * Returns the CSRF token from the cookie (or creates one if missing) + * This is required for pages served as static HTML by nginx (bypassing setCsrfToken middleware) */ function getCsrfToken(req, res) { - const token = req.cookies['csrf-token']; + let token = req.cookies['csrf-token']; + // If no token exists, create one (for static HTML pages served by nginx) if (!token) { - return res.status(400).json({ - error: 'Bad Request', - message: 'No CSRF token found. Visit the site first to receive a token.' + token = generateCsrfToken(); + + // Check if we're behind a proxy (X-Forwarded-Proto header) + const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https'; + + res.cookie('csrf-token', token, { + 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 }); }