← Back to documentation

security-audit-pre-launch

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.

SeverityCountLaunch Blocking?
🔴 Critical2Yes
🟠 High1Yes
🟡 Medium4No (pre-GA)
🟢 Low / Informational3No

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-Policy
  • X-Frame-Options: DENY
  • Strict-Transport-Security
  • Referrer-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

FindingPrevious StatusCurrent Status
F1: JWT hardcoded secretNot fixed🔴 Still not fixed
F2: Email enumerationNot fixed✅ Fixed — generic 200 response
F3: Bootstrap endpointDesign decision🟡 Still not mitigated
F4: Unbounded paginationNot fixed✅ Fixed — le=10000 on all offsets
F5: CSV formula injectionNot fixed✅ Fixed — csv.writer + sanitize
F6: Rate limit header bugBug confirmed✅ Fixed — remaining = limit - current
F7: Error disclosure (ingest)Not fixed🟡 Still not fixed
F8: Open redirect (track)Mitigated🟢 Unchanged (acceptable)
F9: Admin rate limitingNot fixed🟠 Still not fixed
F10: Signup rate limitPartial🟡 Unchanged (partial mitigation)
F11: Public docsBy design✅ Acceptable
F12: JWT expiry 1 yearNot fixed✅ Reduced to 7 days

Launch Readiness Checklist

ControlPython APINode.js APILaunch Ready?
SQL Injection✅ ORM✅ ParameterizedYes
XSS / Input sanitization✅ Full middleware⚠️ BasicYes (Python)
JWT secret validation🔴 Missing startup checkN/ANo
API key secret handling✅ Safe🔴 Raw key to PostHogNo
Rate limiting — auth✅ 5/hour✅ Per-minuteYes
Rate limiting — admin🔴 MissingN/ANo
Error disclosure🟡 Raw exceptions✅ SafeNo (pre-GA)
Security headers🟡 Partial🟡 MissingNo (pre-GA)
HTTPS enforcement🟡 Not at proxy🟡 Not at proxyNo (pre-GA)
Secrets in logs✅ Clean🔴 PostHog keyNo
Hardcoded credentials✅ None✅ NoneYes

Recommended Actions

Before Production Launch (Blocking)

  1. C1 — Python API: Add JWT secret startup validation. Fail hard if JWT_SECRET_KEY == "change-me-in-production".
  2. C2 — Node.js API: Hash API key before passing to trackRegistration() / PostHog. Use existing hashKey() utility.
  3. H1 — Python API: Add @limiter.limit("10/minute") to all /v1/admin/* endpoint handlers.

Before General Availability

  1. M1: Sanitize ingest endpoint error responses — generic message to client, full exception to logs.
  2. M2: Add security headers middleware to both APIs (CSP, HSTS, X-Frame-Options).
  3. M3: Enforce HTTP → HTTPS redirect at nginx/proxy layer.
  4. 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.