Privacy, support and evidence
The controls described here explain how the product is designed to protect your data. Release qualification is separate: a source description is not a completed security review, a production acceptance record, or an independent report.
1. Privacy by default
Your setups, sealed calls and data are private to your account and never pooled with other traders' data. Records retained from earlier products keep the same protection: the data model enforces it at the query layer, so an organisation admin cannot construct a query that returns another member's unaggregated trades. The contract is regression-tested in tests/test_snapshot_org_isolation.py and tests/test_trade_idor.py.
TradeLoop does not connect to your broker. Broker API tokens retained from earlier products are encrypted at the field level (see "Hard rule 3 — Encryption at rest" below). A database dump alone is not sufficient to recover broker access.
We do not sell, license, or syndicate your data. We do not run third-party advertising. We do not feed your data into machine-learning pipelines, our own or anyone else's. Your evidence is computed from your sealed calls, in your account, for your benefit.
2. Support and correspondence
Use the published contact options for product and account questions. No staffed support hours or general response time are confirmed here. For a suspected vulnerability, follow How to report.
Opening an email link does not submit a report or confirm delivery. Security-response targets below are specific to vulnerability reports, not a general support SLA.
3. Service status
The service status page reports the availability it can observe. It does not establish independent security certification, complete incident coverage or future uptime.
Hard rules — enforced by code, not by promise
The rules in this section are the security properties we treat as non-negotiable. Each rule has a code path that implements it. For four of them we have also written a CodeQL query, named in its section, to catch a future change that weakens the property.
Code scanning is not enabled for this private repository today (it needs the ENABLE_CODE_SCANNING repository variable and GitHub Advanced Security), so those queries do not run on changes yet. The code, its tests and the queries are available to a security reviewer on request.
1. Authentication — JWT signature, expiry, audience
JWT signature, expiration, and audience are validated on every authenticated request. The decoder is type-locked: a one-hour password-reset token cannot be replayed against /trades, /analytics, or any other bearer route. The single intentional verification bypass is app/security.py::revoke_token, which decodes an already-expired token to extract its jti for the blacklist. The CodeQL query tradeloop/jwt-verify-disabled is written to catch any other call site (not yet running; see above).
2. HMAC verification — constant-time comparison
Every webhook signature comparison uses hmac.compare_digest. The canonical pattern lives in app/api/broker_webhooks.py::_verify_hmac and is paired with a 5-minute replay window (REPLAY_WINDOW_SECONDS = 300). A regression to == on any digest comparison is what tradeloop/hmac-non-constant-compare is written to catch (not yet running; see above). Per-org webhook signing secrets rotate via app/api/orgs.py; the secret is never logged or echoed.
3. Encryption at rest — Fernet under a dedicated key
Broker tokens are encrypted with Fernet (AES-128-CBC + HMAC-SHA256, authenticated) under a dedicated ENCRYPTION_KEY environment variable. The key derivation is SHA-256 over the encryption key, base64-urlsafe-encoded to Fernet's 32-byte requirement (app/crypto.py::_derive_key). When ENCRYPTION_KEY is unset the dev fallback is SECRET_KEY; production refuses to boot without an explicit ENCRYPTION_KEY (boot guard in app/config.py).
Rotating the JWT signing secret therefore does NOT corrupt stored broker tokens — the two keys are separate. The CodeQL query tradeloop/fernet-key-from-secret is written to catch any new Fernet(settings.secret_key) call site (not yet running; see above).
The KDF is intentionally a single SHA-256 (not PBKDF2 or HKDF) because the input is a high-entropy, server-managed secret rather than a user password. If a future audit calls for HKDF-Expand or PBKDF2, the swap is non-breaking (Fernet treats the key as opaque); we will document the change here in the same commit.
4. SSRF protection — outbound HTTP guarded
Outbound HTTP calls to user-supplied URLs (org outbound webhooks, MT5 server allowlists, custom broker callback URLs) are guarded against private-IP ranges (RFC 1918, link-local, and cloud-metadata addresses). The shared validator lives in app/services/url_safety.py; the MT5-specific guard lives in app/services/broker_service.py. The two canonical attack payloads — 169.254.169.254 (cloud metadata) and 127.0.0.1:6379 (in-VPC Redis pivot) — are explicitly tested in tests/test_org_webhook_ssrf.py and tests/test_mt5_ssrf_guard.py. The CodeQL query tradeloop/ssrf-no-private-block is written to catch a new outbound HTTP path without the guard (not yet running; see above).
5. Production safety — no SQLite in production
The boot sequence refuses to start when ENVIRONMENT=production AND DATABASE_URL contains sqlite (app/main.py::_assert_no_sqlite_in_production). Render's container disk is ephemeral — a production SQLite would silently lose every paid user on dyno restart. The guard fires at module import (before FastAPI is even constructed) so a misconfigured deploy crashes loud rather than serving 200s on an empty database. Tests run with ENVIRONMENT=testing via tests/conftest.py and are not affected.
6. Logging hygiene — credentials never reach the log shipper
Sentry's before_send scrubber strips JWTs, Bearer tokens, broker access tokens, Razorpay payment / order IDs, and India-specific PII (PAN, GSTIN, IFSC, 10-digit phone numbers with optional +91 / 91 country code) from event payloads before they leave the dyno. The scrubber lives in app/main.py::_scrub_string — our own code, not a vendor SDK's hidden patterns. Authorization, Cookie, and X-CSRF-Token headers are always stripped regardless of pattern match.
The application convention is "if it's a credential, it doesn't go in logger.info". The Sentry scrubber is belt-and-braces; the convention is the first line of defence.
7. PII surface — third-party SDKs run through redaction
The same Sentry scrubber covers JWT, Bearer, email addresses, Razorpay IDs, PAN, GSTIN, IFSC, and Indian mobile numbers. Live AI-provider wrappers remain disabled; the current review does not send trader records to OpenAI or other model providers. Any configured PostHog event mirror remains optional and consent-gated. Optional telemetry must pass its consent and redaction controls before any data leaves the service. New third-party SDKs go through the same review — the convention is documented in docs/agent/SECURITY_MODEL.md.
8. Compute integrity — engine modules, not free-form arithmetic
Outcomes in R, intervals, repeat consistency, and every other number in your evidence is computed by a dedicated module in app/engine/. Engine modules are pure-compute (no I/O, no DB, no network), independently tested, and re-used across the API and the export. If you're tempted to add a sum(t.pnl) inline in an API handler, the convention is "stop, write it as an engine call". The frontend's MCP rule (.cursor/rules/tradeloop.md) binds AI assistants to the same convention for traders.
9. Browser and transport controls
The frontend configuration and API middleware set an enforced script policy without 'unsafe-inline'. The static frontend uses same-origin scripts and an explicit origin allowlist; API-served HTML supports per-request nonces. A separate reporting header does not replace that enforced policy. The build validates generated frontend scripts against the configured policy.
Configuration includes HSTS directives, frame restrictions and other browser headers. Actual deployed headers, TLS configuration and host-level protections need deployment verification; a configuration file alone does not prove them. See vercel.json and app/main.py::add_security_headers for the implementation.
10. Admin gate — three layers + 2FA step-up
Admin access is defended by three layers: role=admin on the user row, FOUNDER_EMAILS belt-and-suspenders allowlist (even with role=admin the email must match), and ADMIN_IP_ALLOWLIST (CIDR list, network-level gate). Plus 2FA TOTP step-up (ADMIN_2FA_REQUIRED). Implementation in app/dependencies.py::require_admin.
Where to check release evidence
Check rolling service health at /status. A healthy service is not proof that every workflow or security gate has passed.
Service history
The 30-day uptime grid and 90-day incident window live at /status.
Security gates
The repository defines automated security and release checks. The presence of those checks is not a passing receipt for the current candidate or independent review.
If either public surface looks stale or inconsistent, email [email protected] — that is a product bug, not a documentation nit.
How to report a vulnerability
Email [email protected] with the subject SECURITY: <one-line description>. This page cannot confirm delivery or acknowledgement. Please do not open a public GitHub issue for security vulnerabilities.
Published security-response targets
- Acknowledgement of receipt: 1 business day, 24h max.
- Triage (severity, scope): 3 business days.
- Fix shipped to production: 7 business days for high/critical, 30 days for medium.
- Coordinated public disclosure: within 90 days from initial report.
We don't run a paid bug-bounty programme yet. Confirmed reports are publicly credited at Acknowledgements (the "hall of fame" anchor) once the fix has shipped, with the reporter's permission.
In scope
- The TradeLoop API (
https://api.tradeloop.trade/api/*) - The TradeLoop web app (
https://app.tradeloop.trade/*and any custom domain we operate) - The HMAC-signed inbound broker webhook endpoint
- The HMAC-signed outbound intervention webhook payload format
- The org-scoped data model — any cross-tenant data leak is treated as critical
- Authentication, session management, JWT handling
- Payment processing flow (Razorpay integration)
- Field-level encryption of broker tokens
- The public uptime / incidents endpoints under
/api/v1/status/*
Out of scope
Reports about the following are acknowledged but not prioritised:
- Self-XSS that requires the user to paste arbitrary code
- Vulnerabilities in third-party services we depend on (Render, Vercel, Razorpay, Resend, Sentry, GitHub) — please report those directly to the vendor
- Issues only reproducible on outdated browsers (more than 2 versions behind)
- Rate-limit thresholds (those are policy, not a vulnerability)
- The OpenAPI schema being publicly readable at
/openapi.json— that's intentional in non-production environments only
Severity rubric
We classify reports against the following rubric. The fix-time targets above scale with severity.
- Critical — cross-tenant data leak; unauthenticated remote code execution; payment-amount manipulation; mass account takeover.
- High — single-account takeover; PII leak through an authenticated endpoint; CSRF on a destructive action; HMAC bypass.
- Medium — local privilege escalation; rate-limit bypass on sensitive routes; reflected XSS.
- Low — verbose error messages leaking software versions; open-redirect; CSRF on a non-destructive action.
Independent assurance
No certification date or independent report is confirmed by this page. Source controls and local test results are not SOC 2, ISO 27001, or an independent penetration test.
- SOC 2 Type 1 — not established here.
- SOC 2 Type 2 — not established here.
- ISO 27001 — not established here.
- Independent penetration test — no published report is supplied here.
- Bug bounty programme — no paid programme; public credit requires the reporter's permission.
Independent assurance needs an actual scoped report, its date and its limitations. This page shows no placeholder badges.
Controls to verify for each release
- TLS 1.3 in transit, HSTS preload, no mixed content
- AES-256 at rest (Render Postgres managed encryption)
- Field-level Fernet encryption for broker tokens under a dedicated key (see "Hard rule 3")
- JWT type-locked to access-only on bearer authentication; reset/verify tokens cannot be replayed against the bearer routes (see "Hard rule 1")
- HMAC-signed inbound and outbound webhooks with a 5-minute replay window (see "Hard rule 2")
- Org-scoped data model — a trader's personal trades cannot be read by an org admin, enforced at the query layer
- Production fatal-rails on insecure-default
SECRET_KEY, Razorpay test keys, and missing Redis on multi-worker deploys - Audit log with HMAC chain
- Webhook signing-secret rotation API; secret never logged or echoed
- Sentry
before_sendPII scrubber strips JWT, Bearer tokens, email addresses, broker tokens, Razorpay payment IDs, PAN, GSTIN, IFSC, and Indian mobile numbers (see "Hard rule 6") - CodeQL queries written for four security hard rules, not yet running because code scanning is not enabled for this private repository (
tradeloop/jwt-verify-disabled,tradeloop/hmac-non-constant-compare,tradeloop/fernet-key-from-secret,tradeloop/ssrf-no-private-block) - Public 30-day uptime grid + 90-day incident history at /status, sourced from a 5-minute internal probe sweep
Acknowledgements (Hall of fame)
Researchers who have responsibly disclosed vulnerabilities are credited here once the fix ships and the embargo lifts. None to credit yet — once we have entries, the bullet list gains: the reporter's name (or chosen handle), a short description of the class of finding, the fix commit SHA, and the disclosure date. With the reporter's permission only.
The machine-readable disclosure metadata lives at /.well-known/security.txt per RFC 9116. Both the Contact and Policy directives in that file deep-link back to this page.
Contact
Questions, scoping, or anything that doesn't fit a vulnerability report? Email [email protected].