[object Object]

← back to Hollywood Wallcoverings

fix: strip 3rd-party vendor tokens from product slug (redactVendorSlug, house-brand-safe) + dual-key route resolve — closes standalone slug vendor leak

161125d78a4fdc3e464dccf2fca62846a612d309 · 2026-05-29 22:19:16 -0700 · Steve Abrams

Files touched

Diff

commit 161125d78a4fdc3e464dccf2fca62846a612d309
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri May 29 22:19:16 2026 -0700

    fix: strip 3rd-party vendor tokens from product slug (redactVendorSlug, house-brand-safe) + dual-key route resolve — closes standalone slug vendor leak
---
 server.js | 47 ++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 42 insertions(+), 5 deletions(-)

diff --git a/server.js b/server.js
index 2218fd8..05c33e0 100644
--- a/server.js
+++ b/server.js
@@ -9,6 +9,39 @@ const helmet = require('helmet');
 const path = require('path');
 const fs = require('fs');
 
+/* Vendor-name leak scrub (audit 2026-05-29) — strip any hyphen-delimited segment
+ * carrying a 3rd-party vendor token from a slug/handle shown in this storefront's
+ * UI (the /p/<slug> hrefs + visible-sku handle fallback). "Hollywood" is a DW
+ * HOUSE BRAND and is NOT in the denylist, so it passes through untouched. The
+ * REAL p.handle is kept for the main-store memo-sample / Shop-on-DW links (they
+ * legitimately point at designerwallcoverings.com/products/<real-handle>). The
+ * /p/ route resolver is dual-keyed so both the real handle and the redacted slug
+ * resolve. Longer vendor names precede their prefixes ("lee jofa modern" before
+ * "lee jofa") so the longer match strips first. Verbatim from
+ * dw-domain-fleet/shared/render.js @ ee30c6a. */
+const VENDORS_DENYLIST = [
+  'wolf gordon', 'nina campbell', 'jeffrey stevens', 'versace',
+  'arte international', 'lee jofa modern', 'lee jofa', 'innovations usa',
+  'innovations', 'koroseal', 'schumacher', 'candice olson', 'thibaut',
+  'maya romanoff', 'scalamandre', 'dedar', 'carnegie', 'cole and son',
+  'kravet', 'clarke and clarke', 'brunschwig', 'gp j baker', 'sandberg',
+  'designers guild', 'graham and brown', 'harlequin', 'andrew martin',
+  'ronald redding', 'blithfield', 'coordonne', 'fentucci', 'sister parish',
+  'phillip jeffries', 'fromental', 'westport', 'breegan', 'les ensembliers',
+  'roger thomas', 'stacy garcia', 'de gournay', 'romo', 'farrow and ball',
+  'colefax and fowler', 'sanderson', 'morris and co', 'osborne and little',
+];
+const VENDOR_SLUG_RES = VENDORS_DENYLIST.map(v => {
+  const s = String(v).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
+  return new RegExp('(^|-)' + s + '(?=-|$)', 'gi');
+});
+function redactVendorSlug(slug) {
+  let out = String(slug == null ? '' : slug);
+  for (let i = 0; i < VENDOR_SLUG_RES.length; i++) out = out.replace(VENDOR_SLUG_RES[i], '$1');
+  out = out.replace(/-{2,}/g, '-').replace(/^-+|-+$/g, '');
+  return out || String(slug == null ? '' : slug);   // never empty (route key must be non-empty)
+}
+
 const PORT = process.env.PORT || 9832;
 const __SITE = path.basename(__dirname);
 // Admin catalog (PG-backed) is opt-in via MICROSITE_ADMIN_ENABLED=true. Prod
@@ -146,7 +179,7 @@ function styleKey(p) {
 }
 function sortProducts(list, mode) {
   const titleOf = p => String(p.title || '');
-  const skuOf = p => String(p.dw_sku || p.handle || '');
+  const skuOf = p => String(p.dw_sku || redactVendorSlug(p.handle) || '');
   if (mode === 'sku')   return [...list].sort((a,b) => skuOf(a).localeCompare(skuOf(b)));
   if (mode === 'title') return [...list].sort((a,b) => titleOf(a).localeCompare(titleOf(b)));
   if (mode === 'color') return [...list].sort((a,b) => colorRank(a) - colorRank(b) || titleOf(a).localeCompare(titleOf(b)));
@@ -673,17 +706,17 @@ ${body}
 function cardHtml(p) {
   const cleanTitle = (p.title || '').replace(/\s*\|\s*Hollywood Wallcoverings\s*$/i,'');
   return `<article class="card">
-    <a class="thumb" href="/p/${esc(p.handle||'')}">
+    <a class="thumb" href="/p/${esc(encodeURIComponent(redactVendorSlug(p.handle||'')))}">
       ${p.image_url ? `<img loading="lazy" src="${esc(p.image_url)}" alt="${esc(cleanTitle)}">` : ''}
     </a>
     <div class="info">
-      <a href="/p/${esc(p.handle||'')}">
+      <a href="/p/${esc(encodeURIComponent(redactVendorSlug(p.handle||'')))}">
         <div class="t">${esc(cleanTitle)}</div>
         <div class="sub">${esc(p.dw_sku || '')} · ${esc(NORM_TYPE(p.product_type))}</div>
       </a>
     </div>
     <div class="actions">
-      <a href="/p/${esc(p.handle||'')}">View</a>
+      <a href="/p/${esc(encodeURIComponent(redactVendorSlug(p.handle||'')))}">View</a>
       <a class="cta" href="${esc(memoSampleUrl(p.handle))}" target="_blank" rel="noopener noreferrer">Free Sample</a>
     </div>
   </article>`;
@@ -844,7 +877,11 @@ app.get('/', (req, res) => {
 });
 
 app.get('/p/:handle', (req, res) => {
-  const p = DATA.find(x => x.handle === req.params.handle);
+  // Dual-key: match the REAL handle first (old links + main-store parity), then
+  // fall back to the vendor-redacted slug that the /p/ hrefs now emit.
+  const key = req.params.handle;
+  const p = DATA.find(x => x.handle === key)
+         || DATA.find(x => redactVendorSlug(x.handle) === key);
   if (!p) return res.status(404).set('Content-Type','text/html').send(layout('Not found', '<main><div class="empty">Product not found.</div></main>'));
 
   const cleanTitle = p.title.replace(/\s*\|\s*Hollywood Wallcoverings\s*$/i,'');

← d87438c feat(theme): add ?theme=N toggle to layout() — mirrors dw-do  ·  back to Hollywood Wallcoverings  ·  Add per-site favicon (kills /favicon.ico 404) 3193c50 →