[object Object]

← back to Dw Domain Fleet

audit: standalone storefronts outbound-href vendor leak (cdm/carmel/ntw all leak via buy-CTA+anchor, deploy Steve-gated)

7fd25cac89ea67444259cb54c5808a204cf3bf99 · 2026-05-31 13:34:25 -0700 · Steve Abrams

Files touched

Diff

commit 7fd25cac89ea67444259cb54c5808a204cf3bf99
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 31 13:34:25 2026 -0700

    audit: standalone storefronts outbound-href vendor leak (cdm/carmel/ntw all leak via buy-CTA+anchor, deploy Steve-gated)
---
 .../standalone-outbound-href-audit-2026-05-31.md   | 187 +++++++++++++++++++++
 1 file changed, 187 insertions(+)

diff --git a/output/standalone-outbound-href-audit-2026-05-31.md b/output/standalone-outbound-href-audit-2026-05-31.md
new file mode 100644
index 0000000..8d06b28
--- /dev/null
+++ b/output/standalone-outbound-href-audit-2026-05-31.md
@@ -0,0 +1,187 @@
+# Standalone storefronts — outbound-href vendor-leak audit
+
+**Date:** 2026-05-31
+**Scope:** READ-ONLY. The 3 STANDALONE storefronts (own codebases, NOT on the shared
+`dw-domain-fleet/shared/render.js`). Goal: do they carry the same outbound-href vendor
+leak the fleet fix `a85eb02` closed?
+**Reference fix:** `dw-domain-fleet` commit `a85eb02` — route the PDP buy CTA +
+"View on the main store" anchor + canonical through a CLEAN slug, with an internal
+`/buy/:slug` 302-redirect that resolves the real handle server-side so the raw
+vendor-bearing handle never appears in customer-facing HTML.
+
+---
+
+## TL;DR
+
+| Site | Verdict | Leak vectors | Helpers present | `/buy/:slug` route | Live curl |
+|------|---------|--------------|-----------------|--------------------|-----------|
+| customdigitalmurals | **LEAKS** | buy-CTA, main-store-anchor, card-sample-href | `redactVendorSlug` + denylist ✅ | ❌ missing | ✗ (port down) |
+| carmelwallpapers | **LEAKS** | buy-CTA, "View Full Product" anchor, card-sample-href | `redactVendorSlug` + denylist ✅ | ❌ missing | ✗ (port down) |
+| naturaltextilewallpaper | **LEAKS** | buy-CTA, main-store-anchor, card-sample-href | `redactVendorSlug` + denylist ✅ | ❌ missing | ✗ (port down) |
+
+**Common pattern (all 3):** the fix `a85eb02` was **half-applied** on each standalone —
+`redactVendorSlug()` already scrubs the **canonical / `/p/` link slug / displayed SKU**,
+but the **buy/sample CTA**, the **"View on Designer Wallcoverings" anchor**, and the
+**product-card sample link** still emit the **RAW `p.handle`** straight into the DOM.
+None of the three has the `/buy/:slug` internal-redirect route that `a85eb02` relies on.
+
+**No autonomous patch made — deploy is Steve-gated** (customer-facing + vendor
+confidentiality). Fix details below; ready-to-run version queued at
+`~/.claude/yolo-queue/pending-approval/standalone-vendor-href-leak-2026-05-31.md`.
+
+---
+
+## 1) customdigitalmurals
+
+`/Users/stevestudio2/Projects/customdigitalmurals/server.js`
+
+**Verdict:** leaks-via-buy-CTA + main-store-anchor + card-sample-href.
+**Data confirmation:** 737 / 3663 handles (20%) carry a denylist vendor token
+(versace, arte, clarke, kravet, romo, thibaut, schumacher, …).
+
+**Clean vectors:** JSON-LD `url` (server.js:750) + sitemap `<loc>` (server.js:273) +
+all `/product/` card/rail link slugs (188, 199, 630, 698) use `redactVendorSlug(p.handle)`.
+No `<link rel="canonical">` tag emitted at all (only JSON-LD url, which is scrubbed).
+
+**Leaks (raw `p.handle`):**
+- **server.js:725, 762** — PDP buy CTA:
+  ```js
+  const sampleUrl = `${DW_BASE}/products/${p.handle}#sample`;          // L725
+  <a class="sample-cta" href="${escHtml(sampleUrl)}" ...>Order Memo Sample (free)</a>  // L762
+  ```
+- **server.js:726, 763** — PDP "View on Designer Wallcoverings" anchor:
+  ```js
+  const dwUrl = `${DW_BASE}/products/${p.handle}`;                     // L726
+  <a class="dw-link" href="${escHtml(dwUrl)}" ...>View on Designer Wallcoverings →</a>  // L763
+  ```
+- **server.js:636** — product-card sample link (client JS), uses raw `p.handle`
+  even though `p.slug` (redacted) is on the same object:
+  ```js
+  '<a class="card-sample" href="'+esc('https://designerwallcoverings.com/products/'+p.handle+'#sample')+'" ...>Order Free Sample</a>'
+  ```
+
+**Denylist note:** `redactVendorSlug` correctly strips hyphen-delimited vendor tokens.
+One residual edge case `dwta-65212designerwallcoverings-los-angeles` (glued, no `-` before
+"designerwallcoverings" so the `(^|-)` anchor can't match) — 1 product, DW's own name, low
+priority. Bare `york`/`brewster`/`fabricut`/`stout`/`knoll`/`designtex` not in denylist but
+not currently leaking in data.
+
+---
+
+## 2) carmelwallpapers
+
+`/Users/stevestudio2/Projects/carmelwallpapers/server.js`
+
+**Verdict:** leaks-via-buy-CTA + "View Full Product" anchor + card-sample-href.
+**Data confirmation:** 26 / 2182 products carry a denylist token in `handle` →
+flows into `product_url` + `sample_url` (e.g. `agra-okra-arte-international`,
+`mediterranean-texture-red-wallcovering-versace`,
+`highland-wonder-lagoon-wp-clarke-and-clarke`).
+
+**Clean vectors:** `/p/:handle` route URL + card/rail links use `p.slug` (redacted)
+(server.js:630, 706). Card titles have 0 vendor tokens; vendor label hardcoded
+"Designer Wallcoverings"; no `data-handle`/`data-sku` attrs. No `<link rel="canonical">`
+anywhere → canonical vector N/A.
+
+**Leaks (raw handle via `p.sample_url` / `p.product_url`, built in `shapeForSite`
+server.js:63,67; source `scripts/pull-from-shopify.js:92-93`):**
+- **server.js:428** — PDP buy CTA: `<a href="${p.sample_url}" class="btn btn-sample" ...>`
+  (`sample_url` = `…/products/<RAW-handle>#sample`)
+- **server.js:429** — PDP "View Full Product": `<a href="${p.product_url}" class="btn btn-full" ...>`
+  (`product_url` = `…/products/<RAW-handle>`)
+- **server.js:640** — catalog card `.card-sample` anchor (client-rendered raw `sample_url`)
+
+**Denylist note:** missing `york`/`brewster`/`designtex`/`fabricut`/`stout`/`hygge`
+(none currently in data). Also serializes raw `product_url`/`sample_url` into the
+`/api/products` JSON payload — optional hardening to stop shipping those to the browser.
+
+---
+
+## 3) naturaltextilewallpaper
+
+`/Users/stevestudio2/Projects/naturaltextilewallpaper/server.js`
+
+**Verdict:** leaks-via-buy-CTA + main-store-anchor (canonical + card-title clean).
+**Data confirmation:** 4166 products; 98 handles carry a denylisted-or-required vendor
+token (versace, arte, …, plus `dig-326251-designerwallcoverings-com`).
+
+**Clean vectors:** `/p/` href uses `p.slug||p.handle` (slug scrubbed at L250); card
+title is display `title`; PDP SKU spec (L767) runs through `redactVendorSlug`. No
+`data-handle`/`data-sku`. No `<link rel="canonical">` in `<head>` → canonical N/A
+(also means no self-canonical SEO tag — separate non-security note). `public/corner-nav.js`
+clean.
+
+**Leaks (raw `p.handle`):**
+- **server.js:648** — grid card "Order Memo Sample" CTA (client template):
+  ```js
+  + '<a href="https://designerwallcoverings.com/products/'+p.handle+'#sample" class="sample-cta" ...>Order Memo Sample (free)</a>'
+  ```
+- **server.js:738-739, 760** — PDP primary CTA:
+  ```js
+  const dw_url = `${DW}/products/${p.handle}`;
+  const sample_url = `${dw_url}#sample`;
+  ...
+  <a href="${sample_url}" class="btn-primary" ...>Order Memo Sample (free)</a>
+  ```
+- **server.js:761** — PDP "View on Designer Wallcoverings": `<a href="${dw_url}" class="btn-secondary" ...>`
+
+**Denylist GAP (compounds it):** `VENDORS_DENYLIST` (server.js:39-50) is **missing**
+`cowtan`, `designtex`, `knoll`, `brewster`, `york`, `fabricut`, `stout`, `hygge`,
+**`designerwallcoverings`** — so even the scrubbed surfaces (slug/SKU) leak these.
+Confirmed: handle `dig-326251-designerwallcoverings-com` is NOT stripped.
+
+---
+
+## Fix pattern (mirror a85eb02) — applies to ALL three
+
+Each standalone needs the SAME three-part change:
+
+### A. Add a `/buy/:slug` internal redirect route
+Resolves a redacted slug back to the real handle **server-side** and 302s to the main
+store — raw handle lives only in the 3xx `Location` header, never in DOM. Insert near
+each project's existing `/p/:handle` (or `/product/`) route:
+```js
+app.get('/buy/:slug', (req, res) => {
+  const key = req.params.slug;
+  const p = PRODUCTS.find(x => x.handle === key)
+         || PRODUCTS.find(x => redactVendorSlug(x.handle) === key);
+  if (!p) return res.status(404).send(page404());
+  const dest = `${DW}/products/${p.handle}`;          // carmel: p.product_url || `${DW}/...`
+  const frag = req.query.sample != null ? '#sample' : '';
+  res.redirect(302, dest + frag);
+});
+```
+
+### B. Point the PDP buy CTA + main-store anchor at `/buy/:slug`
+- **customdigitalmurals** L725-726: `sampleUrl` → `/buy/${redactVendorSlug(p.handle)}?sample=1`;
+  `dwUrl` → `/buy/${redactVendorSlug(p.handle)}`
+- **carmelwallpapers** L428-429: `${p.sample_url}` → `/buy/${p.slug}?sample`;
+  `${p.product_url}` → `/buy/${p.slug}`
+- **naturaltextilewallpaper** L738-739/760-761:
+  `const buySlug = encodeURIComponent(redactVendorSlug(p.handle));`
+  `dw_url = /buy/${buySlug}`, `sample_url = /buy/${buySlug}?sample=1`
+
+### C. Fix the client-rendered product-card sample link
+- **customdigitalmurals** L636 → `'/buy/'+(p.slug||p.handle)+'?sample=1'`
+- **carmelwallpapers** L640 → `/buy/${p.slug||p.handle}?sample`
+- **naturaltextilewallpaper** L648 → `'/buy/'+encodeURIComponent(p.slug||p.handle)+'?sample=1'`
+
+### D. (naturaltextilewallpaper + optionally cdm/carmel) close the denylist gap
+Add to `VENDORS_DENYLIST`: `cowtan, designtex, knoll, brewster, york, fabricut, stout,
+hygge, designerwallcoverings`.
+
+### Post-deploy verification (per site, after Steve approves + deploys)
+`curl` `/api/products` + one `/p/<slug>` (PDP) and grep rendered DOM for `products/`
+and each vendor token — expect **0** raw-handle hits. (Note: all three ports were down
+during this audit so only static + `data/products.json` verification was possible; a
+live DOM grep is the confirmation step.)
+
+---
+
+## Hard-rule compliance
+- READ-ONLY: no render/server code edited, nothing deployed, no pm2 restart, no server started.
+- Leaks REPORTED with file:line + proposed fix; NOT patched.
+- Standalone deploys = Steve-gated (customer-facing + vendor-confidentiality) → queued for morning review.
+- No `/dtd` needed: the verdict is factual (leaks found by static analysis + data confirmation),
+  the fix is a direct mirror of the already-decided `a85eb02` pattern, and execution is gated
+  rather than a judgment fork.

← a85eb02 render: route PDP buy CTA + main-store anchor + canonical th  ·  back to Dw Domain Fleet  ·  audit: read-only all-sites vendor-href leak re-verify — flee 3b76655 →