[object Object]

← back to Patterndesignlab

security: stop /api/designers/:slug leaking password_hash+email; require admin auth even under PUBLIC=1 (contrarian findings)

2ae1f2ee906f1fcfe4353038eebe938f63754d2f · 2026-07-06 10:45:10 -0700 · Steve

Files touched

Diff

commit 2ae1f2ee906f1fcfe4353038eebe938f63754d2f
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 6 10:45:10 2026 -0700

    security: stop /api/designers/:slug leaking password_hash+email; require admin auth even under PUBLIC=1 (contrarian findings)
---
 server.js | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/server.js b/server.js
index 4422ad1..ee1a82e 100644
--- a/server.js
+++ b/server.js
@@ -77,6 +77,16 @@ app.use((req, res, next) => {
   res.set('WWW-Authenticate', 'Basic realm="patterndesignlab"').status(401).send('auth required');
 });
 app.use(express.json());
+
+// Admin endpoints must require auth even under PUBLIC=1 — the global basic-auth above
+// steps aside in public mode, so re-check the credentials explicitly here.
+function requireAdmin(req, res, next) {
+  const h = req.headers.authorization || '';
+  const [u, p] = Buffer.from(h.split(' ')[1] || '', 'base64').toString().split(':');
+  if (u === USER && p === PASS) return next();
+  res.set('WWW-Authenticate', 'Basic realm="patterndesignlab-admin"').status(401).json({ error: 'admin auth required' });
+}
+
 app.get('/favicon.ico', (req, res) => res.status(204).end());
 app.use(express.static(path.join(__dirname, 'public')));
 
@@ -207,7 +217,8 @@ app.get('/api/designers/:slug', async (req, res) => {
     const items = await pool.query(
       `SELECT x.id,x.slug,x.title,x.style,x.colorway,x.dominant_hex,x.img,x.price_min,x.seamless,x.created_at
        FROM designs x WHERE x.designer_id=$1 AND ${servedClause('x')} ORDER BY x.created_at DESC`, [d.rows[0].id]);
-    res.json({ designer: d.rows[0], designs: items.rows, count: items.rowCount });
+    const { password_hash, email, ...pub } = d.rows[0]; // public profile: never expose hash or raw email
+    res.json({ designer: pub, designs: items.rows, count: items.rowCount });
   } catch (e) { console.error(e); res.status(500).json({ error: 'query failed' }); }
 });
 
@@ -284,8 +295,8 @@ app.post('/api/license/checkout', async (req, res) => {
   } catch (e) { console.error('checkout error', e.message); res.status(502).json({ error: 'checkout unavailable', detail: e.message }); }
 });
 
-// ---- admin design list (basic-auth already applied above) ----
-app.get('/api/admin/designs', async (req, res) => {
+// ---- admin design list (requireAdmin: enforced even under PUBLIC=1) ----
+app.get('/api/admin/designs', requireAdmin, async (req, res) => {
   try {
     const r = await pool.query(
       `SELECT x.id,x.title,x.style,x.colorway,x.motif,x.technique,x.seamless,x.dominant_hex,
@@ -326,7 +337,7 @@ app.post('/api/designer/apply', (req, res) => {
   res.json({ ok: true, id: rec.id, note: 'Application received — our curation team reviews new designers weekly.' });
 });
 
-app.get('/api/admin/applications', (req, res) => {
+app.get('/api/admin/applications', requireAdmin, (req, res) => {
   let rows = [];
   try {
     const raw = fs.readFileSync(APPLICATIONS, 'utf8');

← d5dbf02 5x: verify security-hardened PDL — 2 clean sweeps, 0 real de  ·  back to Patterndesignlab  ·  fix: flush search box into state.q on every load() so typing 20e53ca →