← back to Consulting Designerwallcoverings Com
TK-22 red-team FINDING 4: fix XFF-spoof rate-limit bypass on /api/intake
b28b7419d70a4155b0e828b14694b2119b1c6dab · 2026-07-27 21:05:01 -0700 · Steve Abrams
The /api/intake rate limiter keyed off the raw, client-controllable
X-Forwarded-For header, so rotating XFF per request bypassed the 5/IP/60s
cap entirely (reproduced: 7 rotating-XFF POSTs all 200), defeating FINDING 1.
Now: app.set('trust proxy','loopback') (nginx->Node over loopback on this host,
env-overridable via TRUST_PROXY) + key the limiter off Express's trusted req.ip
instead of the raw header. Verified: rotating attacker-XFF now trips 429 after 5
while distinct real clients stay allowed; oversized still 413, boot/health OK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit b28b7419d70a4155b0e828b14694b2119b1c6dab
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 27 21:05:01 2026 -0700
TK-22 red-team FINDING 4: fix XFF-spoof rate-limit bypass on /api/intake
The /api/intake rate limiter keyed off the raw, client-controllable
X-Forwarded-For header, so rotating XFF per request bypassed the 5/IP/60s
cap entirely (reproduced: 7 rotating-XFF POSTs all 200), defeating FINDING 1.
Now: app.set('trust proxy','loopback') (nginx->Node over loopback on this host,
env-overridable via TRUST_PROXY) + key the limiter off Express's trusted req.ip
instead of the raw header. Verified: rotating attacker-XFF now trips 429 after 5
while distinct real clients stay allowed; oversized still 413, boot/health OK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
server.js | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/server.js b/server.js
index 7ad2b02..cafd922 100644
--- a/server.js
+++ b/server.js
@@ -20,6 +20,17 @@ const { execFile } = require('node:child_process');
try { require('dotenv').config(); } catch { /* dotenv optional */ }
const app = express();
+// Red-team TK-22 FINDING 4: the /api/intake rate limiter keyed off the raw,
+// client-controllable X-Forwarded-For header, so an attacker who rotated XFF on
+// each request bypassed the 5/IP/60s cap entirely (reproduced: 7 rotating-XFF
+// POSTs all returned 200), defeating FINDING 1's protection. On this host the
+// chain is nginx -> Node over loopback (server: nginx, x-powered-by: Express),
+// with nginx appending the REAL client via $proxy_add_x_forwarded_for. Trusting
+// ONLY the loopback hop makes req.ip resolve the value nginx appended (the true
+// $remote_addr, rightmost-untrusted) and ignore any attacker-injected XFF that
+// arrived before nginx — defeating the rotate-XFF spoof. Env-overridable
+// (TRUST_PROXY) for other topologies; defaults to 'loopback'.
+app.set('trust proxy', process.env.TRUST_PROXY || 'loopback');
app.use(express.json({ limit: '1mb' }));
const PORT = process.env.PORT || 9702;
const PUB = path.join(__dirname, 'public');
@@ -78,7 +89,9 @@ app.get('/intake', (_req, res) => res.sendFile(path.join(PUB, 'intake.html')));
app.get('/api/health', (_req, res) => res.json({ ok: true, client: 'Designer Wallcoverings', at: new Date().toISOString() }));
app.get('/api/client', (_req, res) => res.json(readJSON('client.json', {}))); // public-safe summary for signin page
app.post('/api/intake', (req, res) => {
- const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown').toString().split(',')[0].trim();
+ // req.ip is Express's trusted client IP (derived from XFF only at the
+ // configured trust-proxy hops) — not the raw, spoofable header. See FINDING 4.
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').toString();
if (intakeRateLimited(ip)) {
return res.status(429).json({ ok: false, error: 'Too many submissions — please try again in a minute.' });
}
← fdbc2a7 TK-22 red-team FINDING 2: validate admin bucket writes + ato
·
back to Consulting Designerwallcoverings Com
·
TK-22 admin-UX: add created date+time chip to Low Hanging Fr 4fcf330 →