Security overview
A short tour of the security surface. Each row links to a deeper page.
| Concern | Where enforced | Page |
|---|---|---|
| User auth | better-auth: session cookie or OAuth bearer | Backend → auth middleware |
| OAuth federation (agent ↔ marketplace) | PKCE-S256, public clients, ID token JWKS | OAuth threat model |
| RBAC | Guards trust session role; statement defines actions | Backend → RBAC |
| Open redirect (checkout) | validateReturnUrl() allow-list | Concept → Stripe billing |
| Webhook authenticity | Stripe signature verification | Stripe integration |
| Secrets | AWS Secrets Manager → ECS env | Secrets management |
| PII handling | No request bodies in audit log; cookies HttpOnly + Secure | Data handling & PII |
| PCI scope | Card data never touches marketplace (Stripe Checkout) | Stripe PCI |
| Audit trail | PostHog audit log + stripe_events | Audit logs |
| CORS | FRONTEND_URL explicit allow-list with credentials: true | Config & env vars |
What an attacker can try
-
Phish a user → steal a session cookie. Mitigated by HttpOnly cookies and short-lived sessions. Not bulletproof — invest in 2FA via better-auth's
twoFactorplugin if your risk tolerance demands it. -
Forge a Stripe webhook. Mitigated by signature verification at
StripeWebhookService.verify(). Webhook secret must be set; absent secret = silent failure to verify = open door. -
Open redirect via
returnUrl. Mitigated by allow-list. Attempted bypass returns422 INVALID_RETURN_URL. -
Reuse a stolen OAuth code. Codes are single-use and short-lived. PKCE-S256 prevents replay even if the code leaks in a URL.
-
Replay a refresh token. Refresh tokens rotate on every use. Reuse triggers reuse-detection → the entire refresh chain is invalidated.
-
Mass-enumerate users by email. Sign-in returns the same error for "wrong password" and "no such user." Sign-up reveals duplicates only with verified-account flow.
-
Quota bypass by clobbering
clientReqId. EachclientReqIdis unique; reusing one is a no-op, not a free pass. The agent must mint a fresh UUID per logical attempt; you can't avoid quota by always sending the same UUID. -
Privilege escalation via stale role claim.
marketplace:roleis in the ID token (10h). A demoted user keeps admin access until refresh. Mitigation: short token lifetimes, or move role to a "trust nothing in the token" check that hits the DB.
What we deliberately don't do
- No content scanning on reviews. If you need profanity / abuse moderation, add an external service hook before insert.
- No bot mitigation. Cloudflare in front of the marketplace UI handles edge protection; the backend trusts what gets through.
- No rate limiting per IP. Stripe + bcrypt cost rate-limit the auth path naturally. Add an explicit limiter (Redis + token bucket) if you see abuse.
Reporting
If you find a vulnerability, email security@fleapo.ai (or the project's designated contact). Don't open a public issue.