# Security Policy

## Reporting a vulnerability

**Please do not open a public GitHub issue for security bugs.**

If you've found something that could be exploited, email **security@finisit.com** with:
- A description of the issue
- Steps to reproduce (or PoC)
- The version / commit you tested against (e.g. `git rev-parse HEAD` output)
- Whether you'd like credit on disclosure (default: yes, with a name we mutually agree on)

**Response targets:**
- Initial acknowledgement: within 2 business days
- Triage + severity assessment: within 5 business days
- Fix + deploy for high/critical: within 14 days
- Coordinated disclosure: usually 30–90 days from report, depending on severity

## Threat model — what we defend against

The following attacks are explicitly in-scope; reproductions of these get a fix and credit.

### Authentication / authorization
- Privilege escalation between user and admin sessions (we use separate JWT secrets + a `kind: admin` claim)
- Bypassing role permissions (e.g. an analyst calling `approve_payout`)
- Session fixation, JWT replay across users
- CSRF on admin state-changing endpoints (we use double-submit cookie tokens)
- ADMIN_SECRET reuse after `ADMIN_SECRET_DISABLED=1` is set
- Brute-force login (we rate-limit 5 attempts per IP+email per 5 min)
- 2FA bypass without consuming a recovery code
- Recovery code reuse after consumption

### Money flows
- Tampering with cart prices (we always look up price server-side from Printful)
- Replaying a Stripe webhook to trigger duplicate orders / commissions / promo redemptions (we claim `event_id` atomically, and the `commissions` table has a UNIQUE constraint on `(event_id, role, recipient_id)`)
- Negative-amount or zero-cost orders bypassing checks
- Refund a refunded order, or refund someone else's order
- Approving a payout that's already paid (the `WHERE status='pending_review'` CAS on UPDATE blocks this)
- Exceeding `max_redemptions` on a promo (atomic CAS)
- Exceeding `per_user_limit` on a promo (counted at validate time)

### Data integrity
- Tampering with audit log rows (Postgres triggers raise on UPDATE/DELETE of `admin_audit_log`)
- SQL injection (we use the parametrized neon tagged-template; no string concatenation)
- XSS in admin pages (we escape user-controlled content via `AdminClient.esc`)
- Storing PII for users who exercise GDPR delete (we anonymize: scrub email, name, wallet address; keep transaction records for tax compliance)

### Infrastructure
- Cookie theft (cookies are HttpOnly + Secure + SameSite=Strict)
- HTTPS downgrade (HSTS preload header set)
- Clickjacking (X-Frame-Options: DENY + frame-ancestors 'none')
- MIME sniffing (X-Content-Type-Options: nosniff)
- Open redirects (no user-controlled redirect parameters)

## Out of scope

Reports for the following will be acknowledged but generally won't be fixed:

- DDoS / volumetric attacks (Vercel + Cloudflare upstream handle this; not our layer)
- Self-XSS that requires the victim to paste attacker-supplied JS into their own console
- Missing security headers on static marketing pages
- Email delivery weaknesses (SPF / DKIM / DMARC) when they don't enable spoofing
- Vulnerabilities in third-party SaaS we use (Stripe, Printful, Resend, Vercel, Neon) — report those directly to the vendor
- Reports generated by automated scanners without manual verification
- Brute-force of TOTP codes (60-second window, 6-digit codes, ±30s tolerance, login rate-limited — practical brute force is infeasible)

## Hardening posture

These are already in place — listing for context, not a checklist for reviewers:

| Control | Implementation |
|---|---|
| Auth | bcrypt cost 14 (~1s/hash), JWT in HttpOnly cookies, separate admin secret |
| 2FA | TOTP via `otpauth` (RFC 6238) + 10 single-use bcrypt-hashed recovery codes |
| CSRF | Double-submit cookie token; required on every state-changing admin POST |
| Rate limiting | Postgres-backed token bucket on auth, password reset, refund, payout submit |
| Audit log | Append-only via Postgres trigger; every state change captured with actor_id |
| Webhook | Stripe signature verified before any processing; event_id claimed atomically for idempotency |
| DB | All multi-step money operations in `sql.transaction([...])`; UNIQUE constraints prevent duplicate commissions |
| Secrets | `.env*.local` gitignored; secrets only in Vercel env vars |
| HTTPS | HSTS preload, Secure cookie flag |
| CSP | Locked to known origins; no `unsafe-eval` |
| Headers | X-Frame-Options DENY, X-Content-Type-Options nosniff, Permissions-Policy restrictive |
| Observability | Server + browser Sentry capture on money paths; immutable audit log |

## Vulnerability disclosure program (VDP)

We follow coordinated disclosure. If you find a vulnerability:

1. **Report privately** to security@finisit.com (PGP key in
   `.well-known/security-pgp.asc`)
2. **Don't disclose publicly** until we confirm the fix is deployed
3. **Don't access more data than needed** to demonstrate the issue
4. **Don't disrupt service** — no DoS, no spam, no automated scanners
   that hit prod heavily without coordination

In return, we commit to:

- Acknowledge receipt within **48 hours**
- Provide a triage update within **7 days** (severity, plan, ETA)
- Patch within the SLA below
- Credit you publicly (unless you prefer anonymity) at
  `/security-acknowledgments.html`

### In scope

- `*.finisit.app` (production + previews)
- `/api/*` endpoints
- `/admin/*` (auth bypass + privilege escalation)
- Mobile builds via Capacitor (when published to App Store / Play Store)

### Out of scope

See "Out of scope" section above. Specifically excluded for VDP rewards:
- Self-XSS, social engineering of staff
- Reports requiring physical device access
- Findings against third-party services we use (Stripe, Printful, Resend
  — report those to the respective vendor)
- Missing best-practice headers on pages that don't handle sensitive
  data (we welcome the heads-up but won't reward)
- Pre-production / staging environments unless data is real prod data

### Severity + response SLA

| Severity | Examples | First-fix SLA | Bounty range |
|---|---|---|---|
| **Critical** | RCE, full DB read, auth bypass, money theft | 24h | $500–$2,000 |
| **High**     | Account takeover, privileged action without auth, IDOR on money paths | 72h | $200–$500 |
| **Medium**   | Reflected XSS in authenticated context, CSRF on non-money writes, info leak with PII | 7d | $50–$200 |
| **Low**      | Self-XSS where exploitation is implausible, missing headers, debug pages exposed | 30d | $0 (acknowledgment only) |

Bounty ranges are guidance, not promises. We err on the high side for
clear, well-written reports with reproducible PoCs. Pre-Series-B we
top-cap at $2,000; this scales with the company.

### Safe harbor

We will NOT pursue legal action against researchers acting in good faith
under this policy. We follow the principles of
[disclose.io's safe harbor](https://disclose.io/terms/).

## Disclosure history

(none — this section will populate as reports are processed)

---
Last updated: 2026-05-04
