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