[object Object]

← back to Dw Photo Capture

dwphoto: add SSRF-guarded CORS-scoped /apps/similar proxy route → loopback CLIP :9914 (shipped GATED-STEP-1)

c473b219d44f6f4db2e8e6a51f4e1463f77b5bfb · 2026-07-08 14:59:44 -0700 · Steve

Files touched

Diff

commit c473b219d44f6f4db2e8e6a51f4e1463f77b5bfb
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 8 14:59:44 2026 -0700

    dwphoto: add SSRF-guarded CORS-scoped /apps/similar proxy route → loopback CLIP :9914 (shipped GATED-STEP-1)
---
 server.js | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 54 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index d2204fe..5bfd855 100644
--- a/server.js
+++ b/server.js
@@ -850,7 +850,7 @@ function pipeUpstreamImage(src, res, headers) {
 
 const appHandler = (req, res) => {
   // public (no-auth) paths: health + the home-screen-install assets iOS fetches without creds
-  const PUBLIC = ['/healthz', '/icon-180.png', '/icon-512.png', '/apple-touch-icon.png', '/manifest.webmanifest'];
+  const PUBLIC = ['/healthz', '/icon-180.png', '/icon-512.png', '/apple-touch-icon.png', '/manifest.webmanifest', '/apps/similar'];
   const _p = req.url.split('?')[0];
   if (!PUBLIC.includes(_p) && !checkAuth(req)) {
     res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW Photo Capture"' });
@@ -860,6 +860,59 @@ const appHandler = (req, res) => {
 
   if (u.pathname === '/healthz') return send(res, 200, { ok: true });
 
+  // ── PDP CLIP "More like this" proxy (GATED-STEP-1, shipped) ─────────────────
+  // Public (no basic-auth) but TIGHTLY bounded: NOT a generic proxy.
+  //  · SSRF guard: fixed upstream host+path 127.0.0.1:9914/similar — never user-controlled.
+  //  · Input allowlist: ONLY {dw_sku, hex, style, k}; k capped at 50. Anything else dropped.
+  //  · Output allowlist: only public catalog fields (already visible on the storefront).
+  //  · CORS: reflect an ALLOWED storefront origin only (Access-Control-Allow-Origin locked).
+  // Port 9914 stays loopback-only; this is the ONLY externally reachable surface to it.
+  if (u.pathname === '/apps/similar') {
+    const ALLOWED_ORIGINS = new Set([
+      'https://designerwallcoverings.com',
+      'https://www.designerwallcoverings.com',
+      'https://designer-laboratory-sandbox.myshopify.com'
+    ]);
+    const origin = req.headers.origin || '';
+    const corsOrigin = ALLOWED_ORIGINS.has(origin) ? origin : 'https://designerwallcoverings.com';
+    const CORS = {
+      'Access-Control-Allow-Origin': corsOrigin,
+      'Vary': 'Origin',
+      'Access-Control-Allow-Methods': 'POST, OPTIONS',
+      'Access-Control-Allow-Headers': 'Content-Type'
+    };
+    if (req.method === 'OPTIONS') { res.writeHead(204, CORS); return res.end(); }
+    if (req.method !== 'POST') return send(res, 405, { err: 'POST required' }, CORS);
+    let body = '';
+    req.on('data', c => { body += c; if (body.length > 4096) req.destroy(); });
+    req.on('end', async () => {
+      let p; try { p = JSON.parse(body || '{}'); } catch (e) { return send(res, 400, { err: 'bad json' }, CORS); }
+      const dw_sku = (typeof p.dw_sku === 'string') ? p.dw_sku.trim() : '';
+      if (!dw_sku || dw_sku.length > 64) return send(res, 400, { err: 'dw_sku required' }, CORS);
+      // allowlist + sanitize — ONLY these four fields ever reach the loopback service
+      const hex = (typeof p.hex === 'string') ? p.hex.slice(0, 16) : '';
+      const style = (typeof p.style === 'string') ? p.style.slice(0, 64) : '';
+      const k = Math.max(1, Math.min(parseInt(p.k, 10) || 8, 50));
+      const payload = JSON.stringify({ dw_sku, hex, style, k });
+      try {
+        const r = await fetch('http://127.0.0.1:9914/similar', {   // FIXED host+path — SSRF-safe
+          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload,
+          signal: AbortSignal.timeout(8000)
+        });
+        const j = await r.json().catch(() => ({ results: [] }));
+        // re-emit ONLY public-safe columns (defense in depth even though upstream is already scoped)
+        const results = Array.isArray(j.results) ? j.results.map(x => ({
+          dw_sku: x.dw_sku, image: x.image, pattern: x.pattern,
+          vendor: x.vendor, handle: x.handle || null, score: x.score
+        })) : [];
+        return send(res, 200, { ok: true, results }, CORS);
+      } catch (e) {
+        return send(res, 502, { ok: false, err: 'similar upstream', results: [] }, CORS);
+      }
+    });
+    return;
+  }
+
   // ── Remote-shutter pairing routes ──
   // SSE stream for the PHONE cam page. Registers the cam role; listens for `shoot`.
   if (u.pathname === '/cam/events') {

← 06f5839 visual-search /similar: also return storefront handle (LEFT  ·  back to Dw Photo Capture  ·  FM lookup dash-agnostic + printed-code-first: wallpaper-deta 6eb9013 →