Deep functional review — 2026-07-28

Multi-agent review of overall functionality on branch fix/deep-review-hardening. 18 functional domains swept (~147 routes + src/lib/*); every finding was adversarially verified by two independent lenses (logic + exploitability). Only findings that survived verification are recorded here.

Legend: FIXED = patched on this branch (commit noted). OPEN = filed as a GitHub issue. * = split verdict (one verifier confirmed, one refuted) — real defect, bounded/self-inflicted exploitability.

Coverage caveat: the SCIM/SSO/SAML, tracking, and idempotency verifiers hit a session limit during the automated run; those seven leads (marked ¹) were re-verified by hand against the source rather than by the workflow.


Fixed on this branch

Finding Sev Location Commit
Register stored raw-case email → lockout + case-variant squatting HIGH src/app/api/auth/register/route.ts 6bd5b20
OAuth (Google/GitHub) login bypassed org require_2fa mandate HIGH src/lib/auth/options.ts (signIn) 5cb1e36
SES bounce/complaint suppression fire-and-forget + swallowed HIGH src/app/api/webhooks/ses/route.ts 117de10
Batch/bulk SMS+email skipped per-tenant quota HIGH src/lib/api/batch.ts (checkBatchUsage) c46e53e
Batch email skipped suppression for cc/bcc HIGH src/app/api/v1/emails/batch/route.ts 37fd8f1
Scheduled send never re-checked tenant cap; free hard-cap overshoot HIGH/MED src/app/api/cron/send-scheduled/route.ts f9dac1b
verify-poll stamped verifiedAt on pending rows → dropped from cohort HIGH src/app/api/cron/sms-verify-poll/route.ts 981ae89

The scheduled-send fix replaces the once-per-batch usage snapshot with a per-(org,channel) and per-(tenant,channel) remaining-units budget, reserved synchronously before each send and released on every non-commit path, so both the free-plan hard cap and the per-tenant cap hold exactly within a batch.


Open — HIGH / MEDIUM

Auth / session / 2FA

  • OAuth off-board/demote leaves access tokens live ≤1h — MED — src/lib/auth/offboard.ts:26. api_keys has no user_id; refresh-token-less OAuth grants aren't found by revokeOrgOAuthGrantsForUser. Needs a schema column (api_keys.oauth_user_id) → migration.
  • authorization_code exchange skips current-role re-check — LOW→MED — src/app/api/oauth/token/route.ts:241. The refresh path re-derives role/scope; the code path does not, so a user demoted within the 5-min code TTL still mints a privileged live key.
  • Backup-code single-use TOCTOU* — MED — src/lib/auth/options.ts:181. Non-atomic read-filter-write on Neon HTTP; same code usable twice concurrently.
  • login-code & forgot-password timing enumeration — MED — login-code:56, forgot-password:113. Real branch awaits the SES send; fake branch returns instantly. Dispatch the send without awaiting.
  • Passwordless 2FA-disable step-up = confirm_email* — MED — two-factor/disable/route.ts:47. confirm_email == user.email is not a secret to a session-holder.

Webhooks

  • Bulk webhook replay double-send* — HIGH — v1/webhooks/[id]/replay-failed/route.ts:118. Rows inserted next_attempt_at=now (immediately claimable) and delivered inline via after(), racing the retry cron. Same pattern in test-fire/route.ts:110 (MED).

Cron / delivery

  • retention redaction nulls not-yet-sent scheduled messages — MED — src/app/api/cron/retention/route.ts:157. Age-only filter, no status guard; add AND status IN ('sent','delivered','bounced','failed','canceled').
  • reconcile-registrations retries non-idempotent create — MED — src/app/api/cron/reconcile-registrations/route.ts:100. No attempts cap → a broken 10DLC brand re-mints an orphaned AWS registration ~every 15 min for 7 days.
  • SMS single-send provider-success + persist-fail duplicates — MED — src/app/api/v1/sms/route.ts:308. Catch releases the idempotency lock without storing a response → retry re-sends.

Suppressions

  • Template test-send bypasses suppression — MED — src/app/api/internal/templates/test-send/route.ts:61. Calls real SES/SNS with no isSuppressed check → can message a STOP'd recipient.

Billing

  • Overage cron never bills orgs that downgraded before the 1st — MED — src/app/api/cron/report-overage/route.ts:42. Filters by current planStatus='pro' but bills the previous period; a Pro org that cancels loses its overage.

SCIM / SSO / SAML / tracking / idempotency ¹

  • SAML single-use keyed on unsigned Response ID — MED — src/lib/auth/saml.ts:152. wantAssertionsSigned protects the assertion, not the Response wrapper; re-wrap a captured assertion in a new Response ID to replay. Key on the signed Assertion ID + enforce InResponseTo.
  • SCIM list has no ORDER BY — MED — src/app/api/scim/v2/Users/route.ts:105. LIMIT/OFFSET without a stable sort ⇒ paginated IdP syncs skip/duplicate users.
  • SCIM bearer token unrevocable — MED — src/app/api/internal/scim/route.ts. Only GET+POST; a leaked token (provisions users up to developer) can't be revoked without DB surgery.
  • SSO connection has no cert-rotation / disable / delete — MED — src/app/api/internal/sso/route.ts:183. PATCH only touches enforce_sso + org-policy; one connection per org. Expired IdP cert = SAML lockout with no self-serve fix.
  • Idempotency 90s lock < 300s route budget — MED — src/lib/api/idempotency.ts:18. The 90s TTL is justified only by the 60s MCP self-call bound; a direct v1 send may run up to maxDuration=300, expire the lock, and let a retry re-claim before the DB row lands.
  • Open-tracking pixel accepts unsigned requests — MED — src/app/api/track/open/[token]/route.ts:29. if (sig !== null && !valid) — a missing ?s= skips HMAC entirely (documented deprecation fallback); forge-able opens for any known message_id.

Open — LOW / minor

  • Open dedupe keys on spoofable leftmost X-Forwarded-For hop — track/open/[token]/route.ts:53.
  • No rate-limit on 2FA-disable password check (oracle) — two-factor/disable/route.ts:45.
  • login-code sso_required body is an existence oracle — login-code/route.ts:63.
  • JWT missing sessionVersion/sessionId escapes revocation (legacy tokens, likely already expired) — options.ts:394.
  • require_2fa change omitted from audit metadata — sso/route.ts:269.
  • Internal api-key create doesn't validate tenant_id ownership — internal/api-keys/route.ts:135.
  • Batch email doesn't enforce its 7MB attachment cap — emails/batch/route.ts:44.
  • Tenant ID prefix inconsistency ten_ vs tnt_tenant-resolve.ts:105.
  • inbound-email has no rate-limit before 10MB parse + SNS cert fetch — webhooks/inbound-email/route.ts:36.
  • New orgs never provisioned a default project* — register/route.ts:92.
  • One-click unsubscribe re-fires message.unsubscribed on duplicate POST — unsubscribe/[token]/route-post/route.ts:37.

Cross-cutting theme

Every enforcement check the single-send path performs was missing on the batch path and/or the scheduled-send cron (tenant quota, cc/bcc suppression, warmup, hard-cap-within-batch). The batch and cron fixes above close the send-path cluster; audit any new send surface against enforceUsageLimit + filterSuppressed as the reference.