[object Object]

← back to Corvette Dashboard Viewer

Harden now-public surface: guard /img double-writeHead (ERR_HTTP_HEADERS_SENT on CDN stall) + whitelist /api/new-items gids against log-poisoning

9115bd3e9d35f87a145ad6865dff377deee6f26b · 2026-07-28 22:20:36 -0700 · steve

Files touched

Diff

commit 9115bd3e9d35f87a145ad6865dff377deee6f26b
Author: steve <steve@designerwallcoverings.com>
Date:   Tue Jul 28 22:20:36 2026 -0700

    Harden now-public surface: guard /img double-writeHead (ERR_HTTP_HEADERS_SENT on CDN stall) + whitelist /api/new-items gids against log-poisoning
---
 server.js | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 5d33384..9436254 100644
--- a/server.js
+++ b/server.js
@@ -105,6 +105,11 @@ http.createServer((req, res) => {
       processed = lines.length;
       gids = lines.slice(-limit).reverse();
     } catch (e) {}
+    // Defense-in-depth: a shopify_id is `gid://shopify/<Type>/<n>` or a bare number — it
+    // never contains a quote/semicolon/space. Whitelist to that shape so a poisoned
+    // progress-log line can NEVER reach the interpolated SQL below (the ''-escape stays as
+    // belt-and-suspenders). Fixes the stale "only limit is dynamic — no injection" claim.
+    gids = gids.filter(g => /^[\w:/.-]+$/.test(g));
     if (!gids.length) { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ records: [], processed: 0 })); }
     // palette map (gid-number -> [{hex,pct}]) + fresh descriptor maps my jobs applied
     // (color/style/material), loaded once + cached. These are the values that ACTUALLY
@@ -155,7 +160,10 @@ http.createServer((req, res) => {
   // SSRF-guarded: only https shopify.com CDN hosts are fetched; anything else → placeholder.
   if (p === '/img') {
     const PLACEHOLDER = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
-    const placeholder = () => { if (res.writableEnded) return; res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-store' }); res.end(PLACEHOLDER); };
+    // Guard on res.headersSent (NOT just writableEnded): once we've written the 200 and
+    // started piping, a mid-body CDN stall fires ir.setTimeout -> placeholder() while the
+    // pipe is un-ended, and a 2nd writeHead throws ERR_HTTP_HEADERS_SENT -> process crash.
+    const placeholder = () => { if (res.headersSent || res.writableEnded) return; res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-store' }); res.end(PLACEHOLDER); };
     let tu; try { tu = new URL(u.searchParams.get('u') || ''); } catch { return placeholder(); }
     if (tu.protocol !== 'https:' || !/(^|\.)shopify\.com$/.test(tu.hostname)) return placeholder();
     const ir = https.get(tu.href, (pres) => {

← 635e992 Make BASIC_AUTH a real env override in ecosystem.config.js (  ·  back to Corvette Dashboard Viewer  ·  Close /img socket leak: destroy the response on mid-body CDN 3e6c8b1 →