SECURITY AUDIT · 11 JUN 2026
Full server/ codebase review · 296 JS files · 5-dimension parallel sweep with adversarial verification
Headline risk
Two public-facing flaws drive real-money loss: the crypto deposit webhook can double-credit balance on replay, and Steam login is an open redirect.
Both are reachable without any admin gating. Fix Findings 4 & 5 first.
Network context (provided post-audit): /internal and /commands are gated by Cloudflare Zero Trust Access — admin host only, company-email SSO + admin-system user/pass, whose backend issues the requests. /cron is restricted to the internal cron service. This closes the external attack path, so Findings 1–3 are downgraded to Low (defense-in-depth). Residual: the admin-backend→tradeit-backend hop is server-to-server — if it does not also traverse CF Access, the app layer remains its only (absent) control, exposed to SSRF or internal lateral movement.
Branch note: The audited branch hotfix/claude-graphify diff itself (graphify output + a .claude/settings.json Bash hook) contains no security issues. All findings below are pre-existing in the codebase, surfaced by the full-project audit Ehud requested.
Directly exploitable on public endpoints · no admin gating in front
What — Validates a deterministic sha256(status+type+address+amount+secret+tx_id) hash, then on confirmed credits amount_usd × bonus and inserts a topup — without checking whether tx_id was already processed. The Stripe path guards via getByOrderId (stripeProcessService.js:31–43); crypto path does not.
Exploit — Any re-delivery of a valid confirmed body (provider retry, at-least-once delivery, captured/leaked request) re-validates the identical hash and re-credits real-money balance. No crafted value needed.
Fix — Look up tx_id before crediting; perform check + insert in a transaction with a unique constraint on order_id/tx_id, mirroring Stripe.
What — /login reads redirectTo from the query, atob()-decodes it, and passes it straight to res.redirect(atob(redirectTo)). loginValidation validates only the steamId — no allowlist / relative-path check. Base64 is obfuscation, not validation.
Exploit — Crafted ?redirectTo=<base64("https://evil.com/phish")> → victim authenticates with their real Steam session, then is 302'd to the attacker site (post-login phishing + Referer / auth-context leak).
Fix — After decode, require a single-leading-/ relative path (reject //, /\, absolute URLs) or allowlist internal paths.
Requires specific conditions, or lower real-world impact
siteDisabled kill-switch bypass via substring match MediumWhat — allowPaths = ['lootbear','internal','command','cron'] checked via req.originalUrl.includes(path). Naive substring → any URL containing those words (e.g. /api/v2/user/data?x=command) bypasses the emergency site-disable. Also confirms the privileged routes are intentionally exempt from the only global gate — amplifies Findings 1–3.
Fix — Match on mounted prefix (req.path.startsWith('/api/v2/internal')) and apply real auth so the exemption is no longer a free pass.
What — Tokens = Math.floor(Math.random() * 1e8): non-CSPRNG (V8 xorshift128+, seed-recoverable), 108 keyspace, published on the global Redis tradestate pub/sub channel. Impact bounded by how downstream consumers trust the token; no inbound money-gating validation found.
Fix — crypto.randomBytes(16).toString('hex') / crypto.randomUUID() for any value crossing a trust boundary.
What — Public endpoint (reCaptcha only) interpolates email / subject / message unescaped into the email HTML and the from header. Real but low impact: mail clients sandbox HTML (no JS), and nodemailer encodes address headers — so staff-facing HTML/phishing injection, header injection likely neutralized.
Fix — HTML-escape before interpolation (or send as text); validate email with a strict regex.
External path closed by network controls · in-code auth still recommended for the residual hops below
/api/v2/commands admin surface has no app-layer auth Low / DiDWhat — 20 admin endpoints (add-balance, ban-user, restart) carry zero middleware; adminId is read from req.body. External exploitation is blocked by CF Access in front.
Residual — The admin-system backend calls these routes server-to-server. If that hop does not also traverse CF Access, an SSRF in our backend or a foothold on any host that can reach the Node process bypasses the gate entirely. Also, adminId attribution is unverifiable.
Fix (optional) — Lightweight constant-time internal shared-secret on the router; derive adminId from the authenticated principal, not the body.
/api/v2/internal balance & payout endpoints have no app-layer auth Low / DiDWhat — removeUserBalance, addPendingAddableBalance, processWithdrawSell, oauth2ConsumeBalance mutate balances / payouts from req.body; gated externally by CF Access.
Residual — Same server-to-server hop concern as Finding 01 — these are the highest-value mutations in the app, so the defense-in-depth case is strongest here.
Fix (optional) — Internal shared-secret / mTLS on the router so the app layer is not the only (absent) control on the internal hop.
/api/v2/cron financial / rollback endpoints have no app-layer auth Low / DiDWhat — 35 endpoints (refundBlockIds, cleanReservedItems, :gameId/market recompute) reachable only from the internal cron service.
Residual — Any internal-network foothold that can reach the app can trigger refunds / market recompute, since there is no second check. Lower priority than 01–02.
Fix (optional) — Signed-request header from the cron service if defense-in-depth is wanted on the internal hop.
Areas examined and cleared — existing defenses are sound
SQL
Parameterized throughout — named binds / ?, zero string-concat. ${...} occurrences are booleans / hardcoded columns / enum-whitelisted ORDER BY.
CmdInj
None — no child_process / exec / spawn anywhere in server/share/utils/scripts.
Eval
None — no eval, new Function, or vm usage.
PathTravLOAD DATA path hardened: regex filename allowlist + realpath confinement + O_EXCL (infileStreamFactory.js).
Webhooks
Stripe / NowPayments / Monnect all signature-verified and idempotent, amounts server-side. (Crypto is the exception — Finding 4.)
Secrets
No hardcoded secrets — dotenv-vault + process.env throughout; Dockerfile uses BuildKit secret mounts, runs non-root.
Crypto
AES-256-CBC w/ random IV via createCipheriv (not deprecated createCipher, not ECB). No md5/sha1 for security.
SSRF
None — outbound hosts from env / hardcoded; OAuth2 webhook URL is trusted DB partner config, not user input.
Deser.
None — only JSON.parse; no node-serialize, yaml.load, or prototype-pollution merges.
CORS
No origin reflection. Restrictive CSP + HSTS + nosniff set globally.
Redis
No KEYS in production paths; bulk deletes via hDel.
IDOR
Stripe getTopupTransaction session-scoped (getByOrderId(orderId, steamId)) — not exploitable.
| Priority | Action | Severity | Confidence |
|---|---|---|---|
| 1 | Add idempotency guard + unique constraint to crypto deposit credit path (Finding 4) | High | 9/10 |
| 2 | Validate redirectTo to same-site relative paths on Steam login (Finding 5) | High | 9/10 |
| 3 | Replace originalUrl.includes() with prefix match on the site-disable gate (Finding 6) | Medium | 8/10 |
| 4 | Swap Math.random() trade/sale tokens for crypto.randomBytes (Finding 7) | Medium | 7/10 |
| 5 | Escape / validate Contact-Us email inputs (Finding 8) | Medium | 7/10 |
| 6 | Confirm admin-backend→tradeit-backend hop traverses CF Access; if not, add internal shared-secret to /internal + /commands (Findings 1–3, defense-in-depth) | Low / DiD | — |
Generated 11 Jun 2026 · tradeit.gg · Methodology: 5 parallel domain agents (injection / authz / payments / secrets-crypto / web-misc) + 2 adversarial verification passes · confidence ≥ 7 reported