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_keyshas nouser_id; refresh-token-less OAuth grants aren't found byrevokeOrgOAuthGrantsForUser. 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 branchawaits 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.emailis not a secret to a session-holder.
Webhooks
- Bulk webhook replay double-send* — HIGH —
v1/webhooks/[id]/replay-failed/route.ts:118. Rows insertednext_attempt_at=now(immediately claimable) and delivered inline viaafter(), racing the retry cron. Same pattern intest-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; addAND 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 noisSuppressedcheck → 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 currentplanStatus='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.wantAssertionsSignedprotects the assertion, not the Response wrapper; re-wrap a captured assertion in a new Response ID to replay. Key on the signed Assertion ID + enforceInResponseTo. - SCIM list has no
ORDER BY— MED —src/app/api/scim/v2/Users/route.ts:105.LIMIT/OFFSETwithout a stable sort ⇒ paginated IdP syncs skip/duplicate users. - SCIM bearer token unrevocable — MED —
src/app/api/internal/scim/route.ts. OnlyGET+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.PATCHonly touchesenforce_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 tomaxDuration=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 knownmessage_id.
Open — LOW / minor
- Open dedupe keys on spoofable leftmost
X-Forwarded-Forhop —track/open/[token]/route.ts:53. - No rate-limit on 2FA-disable password check (oracle) —
two-factor/disable/route.ts:45. login-codesso_requiredbody is an existence oracle —login-code/route.ts:63.- JWT missing
sessionVersion/sessionIdescapes revocation (legacy tokens, likely already expired) —options.ts:394. require_2fachange omitted from audit metadata —sso/route.ts:269.- Internal api-key create doesn't validate
tenant_idownership —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_vstnt_—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.unsubscribedon 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.