Please do not open a public issue for a security problem. Report it privately through GitHub's private vulnerability reporting. Include what you found, how to reproduce it, and the impact you expect.
You should get a first reply within a few days. Fixes are released as a patch version, and the advisory is published once users have had time to upgrade.
Only the latest minor release receives security fixes while the project is pre-1.0.
These are the properties we treat as security bugs if broken.
- Tenant isolation.
- The tenant always comes from the API key, never from the request.
- A tenant key can never read or change another tenant's data.
- A test walks every route to check this (
TestTenantIsolationAcrossRoutes).
- API keys.
- 256-bit random tokens; only a SHA-256 hash is stored, and each token is shown once.
- Keys are scoped, expirable and revocable.
- A key cannot mint a key with scopes it does not hold.
- Provider credentials.
- Encrypted with AES-256-GCM under
SECRETS_KEY, and bound to their row. - Secret values are write-only through the API and UI.
- Encrypted with AES-256-GCM under
- Webhooks out.
- Signed with HMAC-SHA256 and a timestamp.
- Tenant-supplied URLs cannot reach private, loopback, link-local (cloud metadata) or CGNAT addresses. The check runs on the resolved IP at connect time.
- Redirects are not followed.
- Webhooks in. Never trusted on their own. The body only identifies the payment, and the gateway is asked for the truth.
- Money.
- Integer minor units only, and every ledger posting must balance.
- Ledger entries are append-only, enforced by a database trigger.
- Settlement is idempotent.
- Web UI.
- The session cookie is encrypted, HttpOnly and SameSite=Strict.
- Forms carry a CSRF token.
- A strict Content-Security-Policy is set, and framing is denied.
- Set
APP_ENV=production. The server then refuses to start without a platform key,SECRETS_KEYandUI_COOKIE_KEY. It also turns off the mock provider unless a tenant opts in. - Back up
SECRETS_KEYtogether with your database credentials. Without it, stored provider credentials cannot be decrypted. - Serve it behind TLS and set
PUBLIC_URLto thehttps://address. - Never set
WEBHOOK_ALLOW_PRIVATE=trueoutside local development. - Prefer database keys with narrow scopes over
BOOTSTRAP_API_KEYS.