← 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 →