[object Object]

← back to Dw Pitch Followup

yolo contrarian-gate fixes: socket-anchored /api/send guard (#1) + suppress audit log (#2)

02b62f2d4c2228f3964479ca4796951595f03f8b · 2026-07-12 12:04:27 -0700 · Steve Abrams

Files touched

Diff

commit 02b62f2d4c2228f3964479ca4796951595f03f8b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 12 12:04:27 2026 -0700

    yolo contrarian-gate fixes: socket-anchored /api/send guard (#1) + suppress audit log (#2)
---
 server.js | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/server.js b/server.js
index d292385..675dc32 100644
--- a/server.js
+++ b/server.js
@@ -441,7 +441,12 @@ app.post('/api/send', async (req, res) => {
     // users may view + draft, but firing client email is restricted to a direct
     // local-loopback operator (someone physically at this Mac). Basic Auth alone is not a
     // strong enough gate to hand the internet a send-as-info@ button.
-    if (req.get('cf-connecting-ip') || req.get('x-forwarded-for')) {
+    // Anchor on the UNFORGEABLE socket origin, not on client-settable headers (contrarian
+    // Hole #1): only a direct local-loopback connection may fire a send. Tunnel requests
+    // arrive from cloudflared (non-loopback socket + cf-connecting-ip); loopback callers
+    // never carry those proxy headers. Header check kept as belt-and-suspenders.
+    const sendRemote = req.socket && req.socket.remoteAddress;
+    if (!LOOPBACK.has(sendRemote) || req.get('cf-connecting-ip') || req.get('x-forwarded-for')) {
       return res.status(403).json({ error: 'Sending is disabled over the public tunnel. Open this tool locally to send — drafting and preview still work remotely.', tunnelBlocked: true });
     }
     const { to, subject, body, samples, list, account: acctNo, company } = req.body || {};
@@ -515,7 +520,11 @@ app.get('/api/suppress', (_req, res) => res.json({ suppressed: [...loadSuppress(
 app.post('/api/suppress', (req, res) => {
   const email = String(req.body?.email || '').trim();
   if (!email) return res.status(400).json({ error: 'email required' });
-  res.json(addSuppress([email]));
+  const result = addSuppress([email]);
+  // Audit trail (contrarian Hole #2): manual suppression is a mutating action any authenticated
+  // user can reach — record who/when so a silent suppress of a real buyer is traceable.
+  try { fs.appendFileSync(path.join(DATA_DIR, 'suppress-log.jsonl'), JSON.stringify({ ts: new Date().toISOString(), email: email.toLowerCase(), ip: clientIp(req), added: result.added }) + '\n'); } catch {}
+  res.json(result);
 });
 
 // GET /api/scan-unsubscribes — find clients who replied "UNSUBSCRIBE" to info@ and suppress them.

← 211aef3 yolo: baseline security headers (nosniff, frame-DENY, no-ref  ·  back to Dw Pitch Followup  ·  chore: v0.3.0 (session close — public tunnel + auth hardenin 93dde31 →