Skip to main content

Security overview

A short tour of the security surface. Each row links to a deeper page.

ConcernWhere enforcedPage
User authbetter-auth: session cookie or OAuth bearerBackend → auth middleware
OAuth federation (agent ↔ marketplace)PKCE-S256, public clients, ID token JWKSOAuth threat model
RBACGuards trust session role; statement defines actionsBackend → RBAC
Open redirect (checkout)validateReturnUrl() allow-listConcept → Stripe billing
Webhook authenticityStripe signature verificationStripe integration
SecretsAWS Secrets Manager → ECS envSecrets management
PII handlingNo request bodies in audit log; cookies HttpOnly + SecureData handling & PII
PCI scopeCard data never touches marketplace (Stripe Checkout)Stripe PCI
Audit trailPostHog audit log + stripe_eventsAudit logs
CORSFRONTEND_URL explicit allow-list with credentials: trueConfig & env vars

What an attacker can try

  1. Phish a user → steal a session cookie. Mitigated by HttpOnly cookies and short-lived sessions. Not bulletproof — invest in 2FA via better-auth's twoFactor plugin if your risk tolerance demands it.

  2. Forge a Stripe webhook. Mitigated by signature verification at StripeWebhookService.verify(). Webhook secret must be set; absent secret = silent failure to verify = open door.

  3. Open redirect via returnUrl. Mitigated by allow-list. Attempted bypass returns 422 INVALID_RETURN_URL.

  4. Reuse a stolen OAuth code. Codes are single-use and short-lived. PKCE-S256 prevents replay even if the code leaks in a URL.

  5. Replay a refresh token. Refresh tokens rotate on every use. Reuse triggers reuse-detection → the entire refresh chain is invalidated.

  6. 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.

  7. Quota bypass by clobbering clientReqId. Each clientReqId is 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.

  8. Privilege escalation via stale role claim. marketplace:role is 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.