← back to Customdigitalmurals
fix: strip vendor tokens from product slug (redactVendorSlug) + dual-key route resolve — closes standalone slug vendor leak
c0e645f7f36b635f531f52f62acd9131c320e589 · 2026-05-29 22:19:53 -0700 · Steve Abrams
Files touched
Diff
commit c0e645f7f36b635f531f52f62acd9131c320e589
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri May 29 22:19:53 2026 -0700
fix: strip vendor tokens from product slug (redactVendorSlug) + dual-key route resolve — closes standalone slug vendor leak
---
server.js | 53 ++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 46 insertions(+), 7 deletions(-)
diff --git a/server.js b/server.js
index 04fba2c..fa6db38 100644
--- a/server.js
+++ b/server.js
@@ -14,6 +14,36 @@ const fs = require('fs');
const PORT = process.env.PORT || 9896;
const DATA_FILE = path.join(__dirname, 'data', 'products.json');
+/* Vendor-slug redaction — copied verbatim from dw-domain-fleet/shared/render.js
+ * (commit ee30c6a). The product handle carries 3rd-party vendor tokens
+ * (e.g. "…-arte-international"); these must never reach a customer-facing URL.
+ * redactVendorSlug strips any hyphen-delimited segment matching a denylisted
+ * vendor. The /product/:handle route dual-keys (raw handle OR redacted slug) so
+ * both old and new URLs resolve. The real handle is KEPT on main-store
+ * (designerwallcoverings.com) links so they still resolve there. */
+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)
+}
+
// Shared catalog module — read-path from dw_unified.microsite_products + admin CRUD.
// Admin catalog (PG-backed) is opt-in via MICROSITE_ADMIN_ENABLED=true. Prod
// stays static-only (data/products.json) so Kamatera doesn't need `pg` or the
@@ -103,7 +133,11 @@ app.get('/api/products', (req, res) => {
const total = results.length;
const pages = Math.ceil(total / per);
- const slice = results.slice((page - 1) * per, page * per);
+ // Expose a vendor-redacted `slug` for the client to build the /product/ href
+ // with (the raw handle leaks vendor tokens). The real handle stays on the
+ // object for the designerwallcoverings.com sample link the card also renders.
+ const slice = results.slice((page - 1) * per, page * per)
+ .map(p => ({ ...p, slug: redactVendorSlug(p.handle || '') }));
res.json({ total, pages, page, per, products: slice });
});
@@ -112,7 +146,9 @@ app.get('/api/products', (req, res) => {
app.get('/api/sliders', (_req, res) => {
const out = [];
for (const a of SITE_RAILS) {
- const items = ALL.filter(p => String(p.aesthetic || '').toLowerCase() === a.toLowerCase()).slice(0, 12);
+ const items = ALL.filter(p => String(p.aesthetic || '').toLowerCase() === a.toLowerCase())
+ .slice(0, 12)
+ .map(p => ({ ...p, slug: redactVendorSlug(p.handle || '') })); // vendor-redacted href key for the client
if (items.length >= 4) out.push({ aesthetic: a, items });
}
res.json({ rails: out });
@@ -173,7 +209,10 @@ app.get('/api/facets', (req, res) => {
// Single product page
app.get('/product/:handle', (req, res) => {
- const p = ALL.find(x => x.handle === req.params.handle);
+ // Dual-key: resolve by the RAW handle (old URLs) OR the vendor-redacted slug
+ // (new vendor-clean URLs the cards/rails now link to). Both round-trip.
+ const key = req.params.handle;
+ const p = ALL.find(x => x.handle === key) || ALL.find(x => redactVendorSlug(x.handle) === key);
if (!p) return res.status(404).send(page404());
res.send(productPage(p));
});
@@ -183,7 +222,7 @@ app.get('/', (_req, res) => res.send(homePage()));
app.get('/sitemap.xml', (_req, res) => {
res.set('Content-Type', 'application/xml');
const urls = ALL.map(p =>
- `<url><loc>https://customdigitalmurals.com/product/${p.handle}</loc><changefreq>monthly</changefreq></url>`
+ `<url><loc>https://customdigitalmurals.com/product/${redactVendorSlug(p.handle)}</loc><changefreq>monthly</changefreq></url>`
).join('\n');
res.send(`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>`);
});
@@ -531,7 +570,7 @@ function load(){
grid.innerHTML=data.products.map(function(p){
var img=p.images&&p.images[0]?'<img src="'+esc(p.images[0])+'" alt="'+esc(p.title)+'" loading="lazy">':'<div style="height:75%;background:var(--line)"></div>';
var price=parseFloat(p.price||0)>0?'$'+parseFloat(p.price).toFixed(2)+' / roll':'';
- return '<div class="product-card" onclick="location.href=\\'/product/'+esc(p.handle)+'\\'">'+
+ return '<div class="product-card" onclick="location.href=\\'/product/'+esc(p.slug||p.handle)+'\\'">'+
img+
'<div class="card-body">'+
'<div class="card-vendor">Designer Wallcoverings</div>'+
@@ -599,7 +638,7 @@ function renderRailSections(rails){
host.innerHTML = rails.slice(0,4).map(function(r){
var cards = (r.items||[]).slice(0,8).map(function(p){
var img = p.images && p.images[0] ? p.images[0] : '';
- var href = '/product/'+encodeURIComponent(p.handle||'');
+ var href = '/product/'+encodeURIComponent(p.slug||p.handle||'');
return '<a class="rail-card" href="'+esc(href)+'">'+(img?'<img src="'+esc(img)+'" alt="'+esc(p.title||'')+'" loading="lazy">':'')+'<span class="rail-card-label">'+esc(p.title||'')+'</span></a>';
}).join('');
var label = r.aesthetic.charAt(0).toUpperCase()+r.aesthetic.slice(1);
@@ -651,7 +690,7 @@ ${headerHtml('')}
"brand": { "@type": "Brand", "name": "Designer Wallcoverings" },
"offers": { "@type": "Offer", "priceCurrency": "USD", "price": p.price || "0", "availability": "https://schema.org/InStock" },
"image": mainImg,
- "url": `https://customdigitalmurals.com/product/${p.handle}`
+ "url": `https://customdigitalmurals.com/product/${redactVendorSlug(p.handle)}`
})}</script>
<div class="product-layout">
← c8239a8 scrub vendor name leaks from products.json source (3132 prod
·
back to Customdigitalmurals
·
reconcile: merge prod sort+density toolbar upgrade with the 301d03d →