[object Object]

← back to Rentv

security: /api/posts + /api/posts/:id never serve DRAFTS to non-admin callers (user-tier past the site auth got all posts incl. unpublished outside-wire content) — force published-only, 404 drafts by id

548ec200e9a994fee0d003520ba6bfcac81b9474 · 2026-08-05 14:27:55 -0700 · Steve Abrams

Files touched

Diff

commit 548ec200e9a994fee0d003520ba6bfcac81b9474
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 5 14:27:55 2026 -0700

    security: /api/posts + /api/posts/:id never serve DRAFTS to non-admin callers (user-tier past the site auth got all posts incl. unpublished outside-wire content) — force published-only, 404 drafts by id
---
 server.js | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index e0327154..093cc1c9 100644
--- a/server.js
+++ b/server.js
@@ -457,13 +457,18 @@ const clean = (s, n) => String(s == null ? '' : s).trim().slice(0, n || 500);
 // list — public callers pass ?status=published; admin gets everything (newest first)
 app.get('/api/posts', (req, res) => {
   let a = readPosts();
-  if (req.query.status) a = a.filter(p => p.status === req.query.status);
+  // Non-admins ONLY ever see published posts — drafts (outside-wire, copyright-sensitive per
+  // Option 1) must never be served to a user-tier caller, even one past the site Basic-auth.
+  if (req.role === 'admin') { if (req.query.status) a = a.filter(p => p.status === req.query.status); }
+  else a = a.filter(p => p.status === 'published');
   a.sort((x, y) => (y.created_at || '').localeCompare(x.created_at || ''));
   res.json({ count: a.length, items: a });
 });
 app.get('/api/posts/:id', (req, res) => {
   const p = readPosts().find(x => x.id === req.params.id);
   if (!p) return res.status(404).json({ error: 'not found' });
+  // A draft is invisible to non-admins — 404 as if it doesn't exist (no guessing draft ids).
+  if (p.status !== 'published' && req.role !== 'admin') return res.status(404).json({ error: 'not found' });
   res.json(p);
 });
 app.post('/api/posts', adminOnly, async (req, res) => {

← 80087c0b Cody-gate fixes: harden PR.date with isNaN guard (was render  ·  back to Rentv  ·  chore: session close — search buyer/seller/broker on closing 493127a2 →