[object Object]

← back to Dw Photo Capture

harden pid interpolation in staging insert (independent-review follow-up)

4d1ee3689e5eaa6844c6c9a2a5448cd6b7d2e94e · 2026-09-22 14:19:26 -0700 · Steve Abrams

Kimi + local codex review flagged the one remaining raw interpolation on the
staging insert: pid was interpolated trusting Shopify always returns a numeric
id. A string pid (API-shape change, error body, or a different code path) would
re-open SQL injection into dw_unified. Validate pid as safe-integer digits
before emitting a bare bigint literal; also avoids Number() precision loss
above 2^53. Belongs with 060357d.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142i1ci5jdMrmc8DNr6RjUS

Files touched

Diff

commit 4d1ee3689e5eaa6844c6c9a2a5448cd6b7d2e94e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 22 14:19:26 2026 -0700

    harden pid interpolation in staging insert (independent-review follow-up)
    
    Kimi + local codex review flagged the one remaining raw interpolation on the
    staging insert: pid was interpolated trusting Shopify always returns a numeric
    id. A string pid (API-shape change, error body, or a different code path) would
    re-open SQL injection into dw_unified. Validate pid as safe-integer digits
    before emitting a bare bigint literal; also avoids Number() precision loss
    above 2^53. Belongs with 060357d.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0142i1ci5jdMrmc8DNr6RjUS
---
 server.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 1b07073..7bbc3c8 100644
--- a/server.js
+++ b/server.js
@@ -2703,8 +2703,14 @@ async function createNewItem(p, b64, dryRun) {
     // price is user input (String(p.price)); emit it only as a validated finite number, never a raw
     // interpolated string — a numeric column can't take a quoted literal, so it must be gated as a number.
     const priceLit = (price != null && Number.isFinite(+price)) ? +price : 'NULL';
+    // pid is a Shopify bigint id: validate it as safe-integer digits rather than TRUST its type. A
+    // string pid (API-shape change / error body / a different code path) must never reach raw SQL
+    // interpolation; emitting bare validated digits also avoids Number() precision loss above 2^53.
+    // (Independent review flagged this raw ${pid} interpolation as the one remaining vector.)
+    const pidStr = (typeof pid === 'number' && Number.isSafeInteger(pid)) ? String(pid) : (typeof pid === 'string' ? pid : '');
+    const pidLit = /^\d{1,19}$/.test(pidStr) ? pidStr : 'NULL';
     const stageSQL = `insert into new_items_staging (dw_sku, mfr_sku, vendor, vid, pattern_name, color, price, specs, shopify_product_id, created_via)
-      values (${pgLit(dwsku)},${pgLit(mfr)},${pgLit(vreg.vendor)},${pgLit(vreg.vid || '')},${pgLit(name)},${pgLit(color)},${priceLit},${pgLit(specsJson)}::jsonb,${pid || 'NULL'},'scan') on conflict do nothing`;
+      values (${pgLit(dwsku)},${pgLit(mfr)},${pgLit(vreg.vendor)},${pgLit(vreg.vid || '')},${pgLit(name)},${pgLit(color)},${priceLit},${pgLit(specsJson)}::jsonb,${pidLit},'scan') on conflict do nothing`;
     execFile(PSQL, ['-d', DW_DB, '-c', 'create table if not exists new_items_staging (id bigserial primary key, dw_sku text, mfr_sku text, vendor text, vid text, pattern_name text, color text, price numeric, shopify_product_id bigint, created_via text, created_at timestamptz default now()); alter table new_items_staging add column if not exists specs jsonb; ' + stageSQL], { timeout: 8000 }, () => {});
     // FileMaker WALLPAPER master — real create (dedupe on Series + Mfr Pattern so a re-run never dupes).
     let fmResult = { committed: false, skipped: 'FileMaker disabled or not configured' };

← 060357d fix SQL injection in create-item staging insert (dollar-quot  ·  back to Dw Photo Capture  ·  auto-data-snapshot: 2026-09-23T03:44:04 (1 data files) — dat a7f6dc5 →