← Back to documentation

security-fixes-pre-launch

Security Fixes — Pre-Launch Hardening

Applied: 2026-04-18 Issue: BUY-3176BUY-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

FindingStatus
JWT expiryAlready 7 days — no change needed
Hardcoded secrets (Node.js)config.ts uses env vars throughout — clean
Python API JWT secretTracked 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"}