Security model
What strikeabox protects, how, and what it deliberately doesn't hold.
What there is to protect
- Practice integrity: balances, accepted terms and results must be exactly what the rules say. Credits have no cash value, but the game must still be fair and checkable.
- Strike API-wallet keys: a key can place and cancel orders on a user's Strike account.
- Sessions: a session token is the only credential of a guest account.
strikeabox holds no wallet seed phrases, no wallet private keys, no passwords, no email addresses and no payment details. Users sign the Builder Connect message in their own wallet; strikeabox only receives the signature.
Strike API-wallet custody
- The server generates the Ed25519 key pair for each Builder Connect. The private key never leaves the server: it is not sent to the browser, logged or written to test artefacts.
- At rest it is encrypted with AES-256-GCM under
KEY_ENCRYPTION_KEY. The additional authenticated data binds the user id and the public key, so a ciphertext copied to another user's row fails to decrypt. Only one key-encryption key is supported: there is no key rotation or re-encryption. Each record stores the key id (KEY_ENCRYPTION_KEY_ID) it was encrypted under, so a changed key is detected; a key that can't be opened is reported (a live box whose close needs it becomesCLOSE_FAILEDwith instructions, and new entries ask the user to connect again). The link is not revoked, so restoring the previous key recovers it; otherwise users connect again. - The key can trade but cannot withdraw: Strike requires a signature from the user's own wallet for withdrawals and only pays out to the connected wallet.
- Disconnect deletes the encrypted key immediately (crypto-shredding). Reconnecting makes Strike revoke the previous key.
- A Strike
401(authentication failed,wallet expired or revoked) marks the linkrevoked.
Why server-side at all: Builder Connect entrusts the key to the builder, and the worker must be able to close positions at their deadline and reconcile with the browser closed.
Residual risk: an attacker with both database access and KEY_ENCRYPTION_KEY could place orders on connected users' Strike accounts until they disconnect, revoke the key or it expires. They could not withdraw.
Network allowlist
The server refuses to start unless every Strike URL is the exact host of the network it serves, over the expected scheme and without a custom port: trading, the user stream and Live mode's market on the execution network (testnet by default), practice market data on the data network. A host of the other network is a configuration error; there is no fallback. Live trading on mainnet also needs LIVE_MAINNET_ENABLED=true; reading mainnet's public market data moves no funds. Mock hosts are accepted only under NODE_ENV=test with STRIKE_ALLOW_MOCK_HOSTS. See Strike integration.
Sessions
- A session token is 32 random bytes (base64url). The database stores only its SHA-256 hash, so a database leak doesn't reveal usable tokens.
- Cookie
sbx_session: HttpOnly, Secure in production, SameSite=Lax, 30 days. Clients that can't use cookies sendAuthorization: Bearer. - Logout revokes the session server-side and closes every WebSocket that was authenticated with it. A socket never outlives its session.
- Guest account creation is rate-limited to 10 per hour per IP. The IP is the TCP peer address;
X-Forwarded-Foris trusted only from the proxies listed inTRUST_PROXY, and a catch-all value is refused at start-up. WhenCLIENT_IP_HEADERnames a header the platform's edge sets and clients cannot forge, a single valid IP address in it is used instead; unset, that header is never read. Authorization: Beareris all-or-nothing: a request that carries it is authenticated by it alone, with no fallback to the cookie.
CSRF and CORS
Every state-changing request (POST, PUT, PATCH, DELETE) to any path must carry X-SBX-CSRF: 1, unless it is authenticated by Authorization: Bearer. The rule doesn't depend on how the URL is spelled, and it covers the Live and social routes as well. Browsers can't add a custom header cross-origin without a CORS preflight, and CORS allows only APP_ORIGIN, with credentials. Bearer-token requests aren't exposed to CSRF because browsers don't attach them automatically. The WebSocket accepts the session cookie only when the Origin header is absent, equals APP_ORIGIN or matches the request's own host.
Quote integrity
Every quoted cell carries an HMAC-SHA256 tag over the cell, band, window, multiplier, probability, S0, sigma and issue time, keyed with QUOTE_SECRET. The server verifies it in constant time and never trusts a client's multiplier without a valid, fresh tag. It then prices the cell again, with the same robust method, before accepting. QUOTE_SECRET is used for quote tags only.
Integrity of the game
- Atomic acceptance. Idempotency, balance reservation, exposure checks, ledger entries, the bet row and its settlement job happen in one transaction. Concurrent taps can't overspend or break caps.
- Immutable terms. A database trigger forbids updates to an accepted bet's terms.
- Settle once. The status change is a guarded update (
WHERE status = 'ACCEPTED') in the same transaction as the ledger entries. - Double-entry ledger. Every movement is a pair summing to zero; tests reconcile balances against it.
- No manual settlement. Operators can only pause new taps. Results come only from recorded evidence.
- Mutation checks confirm the tests fail when idempotency, the status guard, the cutoff or the tag check is removed.
Limits and abuse
| Control | Value |
|---|---|
| Tap rate per user | token bucket, 8 burst, 4 per second |
| Guest sessions per IP | 10 per hour |
| Request body | 16 KiB at most |
| WebSocket | 5,000 connections per process, 20 per client IP; 4 KiB per message, 40 burst and 20 per second; 32 subscriptions per connection |
| Wallet connection attempts | 5 per user and 20 per network in about ten minutes (each one is a call to Strike) |
| Open stake per user | 500 credits |
| Potential payout per cell / global | 20,000 / 500,000 credits |
| Live boxes | 3 open, $200 each, $5,000 per user per UTC day |
| Referrals | once per account, within 24 h of creation, no self-referral, bonus after 5 decided boxes (won or lost; void refunds do not count) |
Rate limits are held in memory per api instance. Running several instances loosens them proportionally; see Known limitations.
What is logged
- Configuration at start-up: names and whether secrets are set, never values.
- Request and job logs at the configured
LOG_LEVEL. - Never: API-wallet private keys,
KEY_ENCRYPTION_KEY,QUOTE_SECRET, session tokens or database passwords.
Public replays omit the user id, the internal box id and the room id, and show a display name only if the user turned on the account-level "show me in feeds" setting.
Secrets in the repository
None. .env files are git-ignored; only apps/server/.env.example with empty values is committed. Fixtures use generated test keys only.
Reporting a vulnerability
Report it privately through the GitHub repository's security advisory form if it is available, or open an issue without exploit details asking for a contact. Please don't test against other users' accounts.