[object Object]

← back to Lawyer Directory Builder

upgrade.ts: harden ?_demo=paid gate against Host-header spoofing

85d7704a36bb303573349d1da5e95ede6405a967 · 2026-05-04 01:19:26 -0700 · SteveStudio2

Replaced req.hostname check (spoofable when Express has 'trust proxy' or
when reverse proxies forward arbitrary Host headers) with strict
NODE_ENV !== 'production' gate. Local-dev preview still works on dev
machines; production traffic can no longer trigger the paid branch via
crafted Host header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 85d7704a36bb303573349d1da5e95ede6405a967
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 4 01:19:26 2026 -0700

    upgrade.ts: harden ?_demo=paid gate against Host-header spoofing
    
    Replaced req.hostname check (spoofable when Express has 'trust proxy' or
    when reverse proxies forward arbitrary Host headers) with strict
    NODE_ENV !== 'production' gate. Local-dev preview still works on dev
    machines; production traffic can no longer trigger the paid branch via
    crafted Host header.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 src/server/upgrade.ts | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/server/upgrade.ts b/src/server/upgrade.ts
index 9ae7d16..d2f0fac 100644
--- a/src/server/upgrade.ts
+++ b/src/server/upgrade.ts
@@ -301,10 +301,11 @@ router.post('/upgrade', async (req, res) => {
 
 router.get('/upgrade/thanks', async (req, res) => {
   const sid = (req.query.session_id as string) || '';
-  // Local-dev preview: ?_demo=paid forces the paid branch when accessed from
-  // a loopback host. Never matches the public domain.
-  const isLocal = req.hostname === 'localhost' || req.hostname === '127.0.0.1';
-  const demoPaid = req.query._demo === 'paid' && isLocal;
+  // Local-dev preview: ?_demo=paid forces the paid branch in non-prod.
+  // SECURITY: was checking req.hostname which is spoofable via Host header
+  // (especially with `trust proxy`). Now gated on NODE_ENV !== 'production'.
+  const isNonProd = process.env.NODE_ENV !== 'production';
+  const demoPaid = req.query._demo === 'paid' && isNonProd;
   let paid = demoPaid;
   let orderId: number | null = demoPaid ? 1 : null;
   let amountPaid = 49900;

← bf9516d Iter 25: /upgrade/thanks — post-Stripe-success editorial con  ·  back to Lawyer Directory Builder  ·  Iter 26: /audit.html — champagne-tier scoring + critique-app 95b1ccc →