Security Audit — BuyWhere Pre-Launch (OWASP Top 10)
Auditor: Rex (CTO, Agent 8ca957f8)
Date: 2026-04-18
Scope: Python API (app/) and Node.js API (/home/paperclip/.rex/workspace/api/src/)
Prior Audit: SECURITY_AUDIT.md by Eng10 (2026-04-04)
Status: 🔴 Launch-Blocking Issues Found — 2 Critical unresolved
Executive Summary
Since the April 4 audit, 4 of the prior findings have been remediated (email enumeration, unbounded pagination, CSV injection, rate limit header bug). However, 2 critical issues remain, including a new critical finding in the Node.js API that was not previously audited.
| Severity | Count | Launch Blocking? |
|---|---|---|
| 🔴 Critical | 2 | Yes |
| 🟠 High | 1 | Yes |
| 🟡 Medium | 4 | No (pre-GA) |
| 🟢 Low / Informational | 3 | No |
Critical Findings
C1 — JWT Secret Hardcoded Fallback — STILL UNFIXED 🔴
File: app/config.py:16
Previous Finding: Yes (Finding 1, April 4 audit)
Remediation Status: Not implemented
jwt_secret_key: str = "change-me-in-production"
The default secret remains hardcoded with no startup validation. If JWT_SECRET_KEY is not set in the environment, all JWTs are signed with a known value. An attacker who reads any public documentation, .env.example, or Docker image layer can forge valid API tokens for any account.
All JWT operations at app/auth.py:50,55,69 use settings.jwt_secret_key directly. No startup check exists in app/main.py.
Required fix (before launch):
# In lifespan / startup event in main.py:
if settings.jwt_secret_key == "change-me-in-production":
raise RuntimeError("JWT_SECRET_KEY environment variable must be set before starting the server.")
C2 — Raw API Key Sent to PostHog Analytics — NEW FINDING 🔴
File: api/src/routes/auth.ts:48, api/src/analytics/posthog.ts:74–95
Previous Finding: No (Node.js API was not audited April 4)
Remediation Status: Not fixed
// auth.ts:48
trackRegistration(rawKey, agent_name, signupChannel, utmSource || null);
// posthog.ts:74–95
ph.capture({
distinctId: apiKey, // raw key sent to 3rd-party analytics
event: 'agent_registered',
...
});
ph.identify({
distinctId: apiKey, // raw key stored in PostHog identity graph
...
});
Every new agent registration sends the raw bw_<token> API key to PostHog as the analytics distinctId. PostHog stores and retains this data. A breach of the PostHog account, or a misconfigured PostHog project with external sharing enabled, would expose all API keys. Keys cannot be rotated without invalidating existing agents.
A hashing utility already exists in posthog.ts:11–12. It is not being used here.
Required fix (before launch): Replace distinctId: apiKey with distinctId: hashKey(apiKey) in both capture and identify calls.
High Findings
H1 — Admin Endpoints Have No Rate Limiting 🟠
File: app/routers/admin.py:110–578
Previous Finding: Yes (Finding 9, April 4 audit)
Remediation Status: Not implemented
Endpoints /v1/admin/stats, /v1/admin/logs, /v1/admin/commissions, /v1/admin/revenue, /v1/admin/kpi, and /v1/admin/analytics have no @limiter.limit() decorator. Any valid API key holder can issue unlimited requests to expensive aggregation and log-scan queries.
Required fix (before launch): Add @limiter.limit("10/minute") to all admin endpoint handlers.
Medium Findings (Pre-GA)
M1 — Raw Exception Messages Returned to Clients 🟡
File: app/routers/ingest.py:334–341
Previous Finding: Yes (Finding 7, April 4 audit)
Remediation Status: Not implemented
errors.append(IngestError(index=idx, sku=item.sku, error=str(e), code=code))
Raw exception strings from database operations (e.g., IntegrityError: duplicate key value violates unique constraint "api_keys_key_hash_key") are returned in API responses. This leaks schema and library version details.
Fix: Return a generic string to the client; log the full exception server-side.
M2 — Security Headers Missing on Both APIs 🟡
Python API: Only X-Content-Type-Options: nosniff is set (in request_validation.py:174).
Node.js API: No security headers middleware detected.
Missing on both:
Content-Security-PolicyX-Frame-Options: DENYStrict-Transport-SecurityReferrer-Policy
Fix: Add headers middleware. For the Python API, a single FastAPI middleware. For the Node.js API, use helmet or manually set headers.
M3 — HTTPS Redirect Not Enforced in Proxy 🟡
Neither nginx.conf nor any load balancer config enforces HTTP → HTTPS redirect at the proxy layer. TLS termination without an HTTP redirect allows unencrypted sessions.
Fix: Add redirect block in nginx or enforce at the load balancer level before US launch.
M4 — Bootstrap Endpoint Enumerable from Public Internet 🟡
File: app/routers/keys.py:27–34
Previous Finding: Yes (Finding 3, April 4 audit)
Remediation Status: Known design, not mitigated
The bootstrap endpoint returns 403 with an explicit message when keys exist, confirming system state to unauthenticated callers. This should be network-restricted (internal VPN / private subnet only) at the proxy layer.
Informational / Low Risk
I1 — SQL Injection Risk: Low (Both APIs) ✅
Python API: SQLAlchemy ORM with parameterized queries throughout. sqlalchemy.text() used only with bound parameters.
Node.js API: All queries use $1, $2, $3 placeholders via pg. No string interpolation in SQL detected.
No action required.
I2 — XSS / Input Sanitization: Good ✅
Python API: Comprehensive request_validation.py middleware strips control characters, HTML-escapes output, and blocks script tags, event handlers, and template injection patterns. Applied to all search and product endpoints.
No action required.
I3 — JWT Expiry Reduced ✅ (Improved)
File: app/config.py:18
jwt_expire_minutes: int = 60 * 24 * 7 # 7 days (was 365 days)
Reduced from 1 year. Tokens can still be revoked via is_active. Further reduction to 1–3 days with a refresh flow is recommended for GA but is not launch-blocking.
Prior Findings — Remediation Status
| Finding | Previous Status | Current Status |
|---|---|---|
| F1: JWT hardcoded secret | Not fixed | 🔴 Still not fixed |
| F2: Email enumeration | Not fixed | ✅ Fixed — generic 200 response |
| F3: Bootstrap endpoint | Design decision | 🟡 Still not mitigated |
| F4: Unbounded pagination | Not fixed | ✅ Fixed — le=10000 on all offsets |
| F5: CSV formula injection | Not fixed | ✅ Fixed — csv.writer + sanitize |
| F6: Rate limit header bug | Bug confirmed | ✅ Fixed — remaining = limit - current |
| F7: Error disclosure (ingest) | Not fixed | 🟡 Still not fixed |
| F8: Open redirect (track) | Mitigated | 🟢 Unchanged (acceptable) |
| F9: Admin rate limiting | Not fixed | 🟠 Still not fixed |
| F10: Signup rate limit | Partial | 🟡 Unchanged (partial mitigation) |
| F11: Public docs | By design | ✅ Acceptable |
| F12: JWT expiry 1 year | Not fixed | ✅ Reduced to 7 days |
Launch Readiness Checklist
| Control | Python API | Node.js API | Launch Ready? |
|---|---|---|---|
| SQL Injection | ✅ ORM | ✅ Parameterized | Yes |
| XSS / Input sanitization | ✅ Full middleware | ⚠️ Basic | Yes (Python) |
| JWT secret validation | 🔴 Missing startup check | N/A | No |
| API key secret handling | ✅ Safe | 🔴 Raw key to PostHog | No |
| Rate limiting — auth | ✅ 5/hour | ✅ Per-minute | Yes |
| Rate limiting — admin | 🔴 Missing | N/A | No |
| Error disclosure | 🟡 Raw exceptions | ✅ Safe | No (pre-GA) |
| Security headers | 🟡 Partial | 🟡 Missing | No (pre-GA) |
| HTTPS enforcement | 🟡 Not at proxy | 🟡 Not at proxy | No (pre-GA) |
| Secrets in logs | ✅ Clean | 🔴 PostHog key | No |
| Hardcoded credentials | ✅ None | ✅ None | Yes |
Recommended Actions
Before Production Launch (Blocking)
- C1 — Python API: Add JWT secret startup validation. Fail hard if
JWT_SECRET_KEY == "change-me-in-production". - C2 — Node.js API: Hash API key before passing to
trackRegistration()/ PostHog. Use existinghashKey()utility. - H1 — Python API: Add
@limiter.limit("10/minute")to all/v1/admin/*endpoint handlers.
Before General Availability
- M1: Sanitize ingest endpoint error responses — generic message to client, full exception to logs.
- M2: Add security headers middleware to both APIs (CSP, HSTS, X-Frame-Options).
- M3: Enforce HTTP → HTTPS redirect at nginx/proxy layer.
- M4: Restrict bootstrap endpoint to internal network via proxy IP allowlist.
Report generated from live codebase scan on 2026-04-18. Findings should be re-verified after remediation.