[object Object]

← back to CelebritySignatures

SECURITY (HOLE 5, blocks live money): paid download link no longer permanent — a Stripe session stays paid forever, so a shared ?sid= URL was infinite free re-download. Now gated to a 1-HOUR window from purchase + max 3 serves/session (persisted download-sids.json), then 410 → contact support. Allows legit retry, kills permanent sharing. Prereq for the live-key flip (TK-10181)

d8cfb50f63fc430176d3db63f887d203dc8de83c · 2026-08-04 10:25:44 -0700 · Steve Abrams

Files touched

Diff

commit d8cfb50f63fc430176d3db63f887d203dc8de83c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 4 10:25:44 2026 -0700

    SECURITY (HOLE 5, blocks live money): paid download link no longer permanent — a Stripe session stays paid forever, so a shared ?sid= URL was infinite free re-download. Now gated to a 1-HOUR window from purchase + max 3 serves/session (persisted download-sids.json), then 410 → contact support. Allows legit retry, kills permanent sharing. Prereq for the live-key flip (TK-10181)
---
 .gitignore |  1 +
 server.js  | 16 +++++++++++++++-
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 44c588f..8ae9c19 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ data/download-ledger.jsonl
 data/admin-token.txt
 data/leaderboard.json
 .env
+data/download-sids.json
diff --git a/server.js b/server.js
index 49401c8..a25a670 100644
--- a/server.js
+++ b/server.js
@@ -423,13 +423,27 @@ ${paid ? `<div class="ok">✓</div><h1>Order confirmed</h1>
       const sid = url.searchParams.get('sid') || '';
       if (!STRIPE_TEST_KEY) return sendJSON(res, 503, { ok: false, error: 'downloads require payment — not configured yet' });
       if (!/^cs_test_[A-Za-z0-9]+$/.test(sid)) return sendJSON(res, 402, { ok: false, error: 'payment required — purchase this download first', purchase: '/api/signature-checkout' });
-      let paid = false;
+      let paid = false, sessionCreated = 0;
       try {
         const s = await (await fetch(`https://api.stripe.com/v1/checkout/sessions/${sid}`, { headers: { Authorization: `Bearer ${STRIPE_TEST_KEY}` } })).json();
         // gate fulfillment on the settled session (status===complete), not just payment_status
         paid = s.status === 'complete' && s.payment_status === 'paid' && String(s.metadata?.upload_id) === x.id;
+        sessionCreated = s.created || 0;
       } catch {}
       if (!paid) return sendJSON(res, 402, { ok: false, error: 'payment not verified' });
+      // HOLE 5 FIX: a paid Stripe session stays paid forever, so a shared ?sid= URL
+      // would be permanent free re-download. Gate the file to a SHORT WINDOW (1h from
+      // purchase) AND a small per-session use cap (3), so a leaked/shared link dies
+      // fast but a buyer whose download drops can retry. Beyond that → 410, contact support.
+      if (sessionCreated && (Date.now() / 1000 - sessionCreated) > 3600) {
+        return sendJSON(res, 410, { ok: false, error: 'this download link has expired (1-hour window) — contact info@designerwallcoverings.com to re-download' });
+      }
+      const sidUse = await load('download-sids.json', {});
+      if ((sidUse[sid] || 0) >= 3) {
+        return sendJSON(res, 410, { ok: false, error: 'this download link has been used its maximum times — contact info@designerwallcoverings.com to re-download' });
+      }
+      sidUse[sid] = (sidUse[sid] || 0) + 1;
+      await store('download-sids.json', sidUse);
       // Idempotent credit: claim the sid ATOMICALLY in-process (closes the concurrent
       // double-credit race — has+add are synchronous, no await between them), then confirm
       // against the persistent ledger so a restart can't re-credit an already-paid sid.

← f2fc2ab payments(TEST): Cody gate fixes — atomic USED_SIDS guard (cl  ·  back to CelebritySignatures  ·  Live-mode switch (prep for go-live, defaults OFF): STRIPE_LI cc656ee →