Security Fixes — Pre-Launch Hardening
Applied: 2026-04-18 Issue: BUY-3176 → BUY-3177 Audit source: BUY-3161
Fix 1 — Affiliate redirect domain allowlist
File: api/src/routes/redirect.ts
Problem: The /r/:affiliateSlug/:productId route redirected to destination_url pulled from the database without validating the target domain. A corrupted or malicious DB record could redirect users to attacker-controlled sites (open redirect, CWE-601).
Fix: Added isAllowedDestination() which parses the URL with new URL(), strips www., and checks against a set of permitted merchant hostnames. On mismatch, the request is rejected with 403 and a warning is logged. No redirect is issued.
Allowlist (default): lazada.sg, shopee.sg, bestdenki.com.sg, amazon.sg, courts.com.sg, harvey-norman.com.sg, challenger.sg, qoo10.sg
Env override: Set AFFILIATE_ALLOWED_DOMAINS (comma-separated) to extend the list without a redeploy.
Fix 2 — CORS origin restriction
File: api/src/server.ts (line 24)
Problem: app.use(cors()) — wildcard, allowing any origin to make credentialed cross-origin requests to the API.
Fix: Replaced with an explicit origin allowlist:
app.use(cors({
origin: (process.env.CORS_ALLOWED_ORIGINS || 'https://us.buywhere.com,https://buywhere.ai').split(',').map((o) => o.trim()),
credentials: true,
}));
Env override: Set CORS_ALLOWED_ORIGINS (comma-separated) to adjust for staging.
Fix 3 — Security response headers
File: api/src/server.ts
Problem: No X-Content-Type-Options or X-Frame-Options headers were set, leaving the API open to MIME-sniffing attacks and clickjacking via iframe embedding.
Fix: Added global middleware (applied before all routes):
app.use((_req, res, next) => {
res.set('X-Content-Type-Options', 'nosniff');
res.set('X-Frame-Options', 'DENY');
next();
});
Not applicable / already handled
| Finding | Status |
|---|---|
| JWT expiry | Already 7 days — no change needed |
| Hardcoded secrets (Node.js) | config.ts uses env vars throughout — clean |
| Python API JWT secret | Tracked separately in BUY-3166 (assigned to Sol) |
Verification steps
# 1. Security headers
curl -I https://api.buywhere.ai/health
# Expect: X-Content-Type-Options: nosniff, X-Frame-Options: DENY
# 2. CORS — blocked origin
curl -H "Origin: https://evil.com" -I https://api.buywhere.ai/health
# Expect: no Access-Control-Allow-Origin header in response
# 3. Redirect — allowed domain (replace IDs with real values)
curl -I https://api.buywhere.ai/r/<slug>/<productId>
# Expect: 302 to lazada.sg / shopee.sg / etc.
# 4. Redirect — blocked domain (if you can set a test DB record)
# Expect: 403 {"error":"Destination not permitted"}