0%
Vibe44 Logo

Security Audit

Transparency report from a comprehensive OWASP-aligned security audit. Share this with enterprise prospects, compliance teams, and security-conscious customers as proof of production-grade posture.

Audit Summary

PropertyValue
ScopeFull-stack: frontend, backend, database
StandardOWASP Top 10 (2025)
Critical Issues0
High Issues0
Medium Issues0
Low / Informational0 actionable
Tables Audited32
Server Actions28
API Routes21 (groups)
Webhook Endpoints5 (one per payment provider)
Unit Tests900 tests across 84 unit test files
E2E Tests9 suites (Playwright)
ResultPASS — Zero Critical Vulnerabilities

Authentication Layer

Audit Scope

Every enabled auth pathway: email/password, OAuth (Google/GitHub), magic links, passkeys (WebAuthn), TOTP MFA, full session lifecycle (creation, refresh, destruction).

Findings

ControlStatusImplementation
Session management✅Supabase Auth + PKCE. JWTs with httpOnly, secure, sameSite=lax cookies.
Password requirements✅Zod: min 8 chars. Supabase handles Argon2id hashing.
OAuth callback✅/auth/callback exchanges code server-side. PKCE code_verifier prevents interception.
MFA enforcement✅Universal via proxy.ts. Queries getAuthenticatorAssuranceLevel(). Fail-closed: errors redirect to /login?mfa=error.
Passkey (WebAuthn)✅60s challenge expiry.credential_id + public_key in passkeys table. Post-login AAL check triggers inline MFA.
Session invalidation✅signOut() after password reset. /verified clears stale sessions.
Step-Up Auth✅High-risk actions (password changes, MFA disablement) require current password re-verification to prevent hijacking.
Duplicate email✅Inspects identities array — generic toast, no account existence leak.
Callback errors✅Parses error, error_code, error_description. Redirects to /login with toast.

Server Action Auth Pattern

Every Server Action follows this exact sequence:

const user = await requireUser();
const parsed = schema.safeParse(input);
if (!parsed.success) return { success: false, error: "Invalid input" };
const [row] = await db.insert(table).values(values).returning({ id: table.id });
revalidatePath("/dashboard");
revalidateTag(CACHE_TAGS.dashboard, "max");

Verified: Server Actions use centralized requireUser() / verifyAdmin() guards before validation. No action trusts client-provided user IDs. No action uses cookies() for auth.


️ Authorization & Access Control

Row-Level Security (RLS)

All 32 tables have RLS enabled with data isolation policies.

PatternSQL
User reads own dataUSING ((select auth.uid()) = user_id)
User writes own dataWITH CHECK ((select auth.uid()) = user_id)
Super-Admin overrideEXISTS (SELECT 1 FROM profiles WHERE id = (select auth.uid()) AND role = 'super-admin')
Org membershipEXISTS (SELECT 1 FROM org_members WHERE org_id = [table].org_id AND user_id = (select auth.uid()))
Avatar Upload✅ — Resolved storage 403 by migrating api/user/avatar to service_role client (bypassing RLS) while maintaining identity verification via session check.

Verified: Every table uses (select auth.uid()) subquery form for per-query caching.

Ownership Verification

Every mutation includes ownership gates:

.where(and(
  eq(table.id, resourceId),
  eq(table.userId, user.id)  // Ownership gate
))

Verified: No mutation modifies resources without ownership check.

Authenticated AI support conversations also resolve caller-supplied conversationId values through getConversationForUser() before history reads, message inserts, broadcasts, status updates, or escalation ticket creation. Guests continue to use local conversation persistence, while authenticated conversation IDs are treated as untrusted references until matched to the effective user.

Super-Admin Protection

  • Self-shielding — super-admin accounts cannot be deleted via super-admin UI
  • Impersonation — HMAC-signed cookie context, 1-hour auto-expiry, read-time super-admin/target revalidation, audit trail
  • Context propagation — proxy.ts injects x-impersonating-* headers for Server Components and audit logging
  • getEffectiveUserId() — read/write isolation during impersonation; PATCH /api/user/profile correctly targets the impersonated user context
  • Layout Synchronization — Reactive CSS variables (--topbar-height) manage real-time UI reflow across decoupled components (Sidebar, Banner) to eliminate jitter during session transitions
  • Activity logging — 36 typed ActivityAction categories in activity_logs
  • Resource Safety & Deletion Guards — Irreversible operations (e.g., deleting projects) require mandatory AlertDialog confirmation to prevent accidental data loss.
  • Resource Capping — Tier-based limits (defined in lib/config/pricing.ts) are enforced server-side and UI-disabled to prevent resource exhaustion and drive conversions.

️ SSRF Protection (A01 / A05)

lib/security/ssrf.ts — isAllowedUrl() + sanitizeUrl():

  • Blocks private IPs (10.x, 172.16-31.x, 192.168.x, 127.x)
  • Blocks cloud metadata (169.254.169.254, Azure IMDS, GCP)
  • Rejects non-HTTP(S) schemes (file://, ftp://, gopher://, data:)
  • Detects numeric IP obfuscation (octal, hex, decimal)
  • 25+ unit tests covering all bypass techniques

Verified: User-controlled outbound URL fetches are blocked. Provider-returned Murf audioFile URLs are also passed through sanitizeUrl() before fetch so voice delivery keeps the SSRF boundary active.

Cryptographic Security (A04)

lib/security/crypto.ts — centralized secure random generation:

FunctionBackingUsed In
secureRandomString(length)crypto.randomBytes() → base36Affiliate referral codes, upload paths
secureRandomHex(length)crypto.randomBytes() → hexDodo webhook event IDs

Verified: Zero Math.random() in security-sensitive server code. All randomness uses crypto.randomBytes().


Input Validation

Zod on Every Boundary

Every Server Action and API route validates input with Zod before any DB operation.

const schema = z.object({
title: z.string().min(1).max(200),
  description: z.string().max(2000).optional(),
});
const parsed = schema.safeParse(input);
if (!parsed.success) {
  return { success: false, error: "Invalid input" };
}

Verified: No action accepts raw, unvalidated input. No schema uses z.any() or z.unknown() without refinement. API and Server Action responses across user, support, developer, super-admin, checkout, upload, and voice surfaces are sanitized to remove verbose Zod validation error strings and use centralized i18n/domain keys, preventing internal schema structures or provider details from leaking. Guest checkout, passkey login, and account deletion failures return stable user-facing messages while preserving root causes in server logs and Sentry.

Upload signing validates bucket, declared MIME type, and size metadata before issuing a Supabase signed upload URL. Stored object extensions are derived from the validated MIME allowlist, not the client-provided filename, so invoice.html with application/pdf stores as .pdf or fails if no safe mapping exists. Production bucket MIME and size policies remain mandatory server-side enforcement for direct browser-to-storage uploads.

XSS Prevention in Emails

escapeHtml() sanitizes user-provided data (user.fullName) before HTML email template injection.


Webhook Security

HMAC-SHA256 Verification

All 5 providers verify request authenticity:

ProviderHeader(s)Algorithm
Stripestripe-signatureHMAC-SHA256
Polarwebhook-id + webhook-timestamp + webhook-signatureHMAC-SHA256
Lemon Squeezyx-signatureHMAC-SHA256
Dodowebhook-id + webhook-timestamp + webhook-signatureHMAC-SHA256
Creemx-creem-signatureHMAC-SHA256

Verified: No webhook processes payload without signature validation. crypto.timingSafeEqual() for constant-time comparison.

Idempotency

webhook_logs tracks every event by event_id + provider:

  1. Skip existing success rows
  2. Skip active processing rows younger than 5 minutes
  3. Reuse failed and stale processing rows with attempts + 1
  4. Execute handler only after the row is claimed
  5. Record success/failure with attempt count

Verified: Duplicate deliveries cause zero duplicate mutations, while transient failures remain retryable.

Billing state adds provider-resource idempotency below the webhook delivery layer:

  • subscriptions(provider, provider_subscription_id) is unique when present, and subscription sync upserts on that key.
  • purchases(provider, provider_payment_id) is unique when present, and duplicate purchase sync returns the existing purchase ID.
  • Creem rejects webhook payloads missing a durable event.id.

Checkout Error Containment

Checkout creation responses return generic translated errors (checkoutCreateError, checkoutSessionError) while provider details are captured server-side. Guest checkout modals display the same stable localized failure copy instead of propagating thrown backend strings. This keeps provider exception text, account identifiers, and SDK diagnostics out of client-visible responses.

Account Deletion Safety

Permanent account deletion is fail-closed for paid users. The deletion service selects every active-like subscription row for the user and cancels each stored provider subscription before local destruction begins. If any provider subscription lacks a provider id, uses an unsupported provider, or fails cancellation, local profile, storage, waitlist, and auth deletion do not proceed. When deletion proceeds, user-owned storage objects are removed from configured buckets, waitlist PII is removed by email, and Supabase Auth deletion drives the profile cascade. The user remains recoverable when any critical external cleanup fails. Unexpected deletion failures are logged with root-cause detail server-side while callers receive stable deletion failure copy.


Transport & Headers

CSP (Content Security Policy)

Nonce-based via proxy.ts:

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://*.supabase.co wss://*.supabase.co;
  • Per-request cryptographic nonce
  • strict-dynamic allows trusted script chains
  • JSON-LD scripts receive the same nonce via <JsonLd nonce={nonce} />
  • Inline styles permitted (Tailwind CSS requirement)

CSRF Protection

HMAC-bound double-submit cookie + future-timestamp guard + 1-hour token expiry:

  1. Server generates token with embedded timestamp, nonce, and user-bound HMAC → httpOnly cookie
  2. Client sends via X-CSRF-Token header (csrfFetch() auto-injection)
  3. Server validates cookie ↔ header match
  4. Server recomputes the HMAC with the authenticated user ID and rejects forged or cross-user tokens
  5. Future-timestamp guard rejects clock skew attacks
  6. Token expiry enforcement — tokens older than 1 hour are rejected (prevents replay)
  7. Cookie Seeding Protocol: To resolve the Next.js Server Component limitation (which prevents setting cookies during RSC rendering), proxy.ts dynamically seeds or refreshes the __csrf_token cookie for authenticated users during page requests, including public marketing and invite routes. Stale or cross-user tokens are rejected and replaced before Server Components read cookies.
  8. Root Client Context: app/layout.tsx reads the middleware-seeded cookie and wraps the application tree in CSRFProvider, making csrfFetch() available to authenticated client components rendered outside (protected).
  9. oRPC Transport Header: The same-origin browser oRPC client reads the CSRF cookie and sends X-CSRF-Token for RPC calls through /api/rpc.

Verified: All authenticated state-mutating requests are CSRF-protected. Token TTL and HMAC/user binding are enforced in proxy.ts middleware. /api/support/chat is not a global CSRF exemption; only guest chat requests without an authenticated session bypass CSRF so public support remains available. Public authenticated flows such as organization invite acceptance use csrfFetch() and the root CSRF provider.

Security Headers

HeaderValuePurpose
X-Content-Type-OptionsnosniffMIME sniffing prevention
X-Frame-OptionsDENYClickjacking prevention
Referrer-Policystrict-origin-when-cross-originReferrer leakage
Permissions-PolicyRestrictiveUnused browser API lockdown
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadHSTS

Redirect Security

All checkout and billing portal return URLs must be constructed using buildSameOriginUrl() from @/lib/security/redirects. This explicitly checks the given URL against the allowed application origin and a whitelist of valid return paths, thwarting open-redirect attacks that try to hijack the post-payment flow.

Allowed Origins (Cross-Origin Resource Sharing)

To mitigate Server-Side Request Forgery (SSRF) and restrict unauthorized asset loading, the next.config.ts explicitly defines allowed remote patterns for external resources (e.g., Supabase storage, Google user avatars). Allowed dev origins are strictly configured to prevent malicious domain loading.


Rate Limiting & DDoS

Edge Rate Limiting

Upstash Redis in proxy.ts middleware:

TierRequests/WindowWindow
FreeConfiguredPer-tier
StarterConfiguredPer-tier
ProConfiguredPer-tier

Limits defined in lib/config/pricing.ts — single source of truth.

Route-Level API Rate Limiting

Beyond the edge middleware, an exhaustive inline checkRateLimit(request) higher-order function pattern enforces rate limiting at the route handler level.

Verified: Enforced across 56+ public and authenticated API route files, including Super-Admin /api/super-admin/* and User-management /api/user/* endpoints, securing sensitive mutations and query paths from burst abuse.

Circuit Breaker

  1. Track violation count per IP in Redis
  2. 5 violations → auto-ban for configurable duration
  3. Banned IPs get 429 without touching app layer

Verified: Rate limiting applied before auth — protects the auth layer itself from brute force.

Graceful Degradation

When Redis is unavailable:

Production rate limiting is enforced even if Redis credentials are missing. In that configuration the edge limiter attempts Redis and fails closed, and authenticated health reports Redis as unhealthy so the deployment cannot be treated as launch-ready. The public /api/health endpoint exposes only minimal liveness metadata.

Client IP attribution is centralized in getClientIp(). Forwarding headers are trusted only on Vercel or when TRUST_PROXY_HEADERS=true is set behind an ingress layer that strips untrusted forwarding headers.

Rate LimiterFailure ModeRationale
rateLimit() (tier-based)Fail-openAvailability over strictness for general requests
edgeRateLimit() (middleware)Fail-closedSecurity-critical path — block unknown traffic

️ Database Security

Connection Security

  • TLS on all connections
  • Supabase pooler (port 6543, transaction mode for serverless)
  • Drizzle ORM parameterizes all queries — zero SQL injection surface

Sensitive Data

DataProtection
PasswordsArgon2id (Supabase Auth) — never in app
API keysSHA-256 hashed (key_hash), original shown once
Webhook secretsEnv vars only — never in DB or client
IP addressesSHA-256 hashing before affiliate storage (GDPR)
PII in analyticsEmail excluded from trackSignup(). Third-party analytics scripts/events require explicit analytics consent. Sentry PII and Session Replay are opt-in.
Passkey keysPublic key only — private key never leaves device

Pagination & Memory

List queries are bounded before execution. Cursor-based reads cap at 100 rows per page. Offset-based reads cap at 200 rows. Webhook administration list reads clamp requested limits to 100 and project only the fields required by retry, list, and detail views.

Data Minimization

Backend reads use explicit Drizzle projections instead of bare .select(). Dashboard sales profile reads, webhook log queries, and background subscription/trial reminder profile reads return only the columns consumed by the caller.

Atomic Operations

deduct_credits() uses FOR UPDATE row locking — prevents race conditions in concurrent AI usage. requestPayout() uses serializable transactions — eliminates TOCTOU race conditions between payout eligibility checks and deduction, ensuring zero data loss during concurrent credit events.

Security Logging (A09)

auth_events table — append-only RLS:

EventTriggerData
login_successSuccessful authUser ID, IP, method
login_failureFailed authIP, email, reason
mfa_challengeMFA initiatedUser ID, factor type
mfa_failureMFA failedUser ID, IP
ip_bannedCircuit breakerIP, violation count
brute_force_alertThreshold breachIP, attempts, window

logAuthEvent() records all events. checkAndAlertBruteForce() triggers on sliding window threshold breach.

Security Dashboard — real-time in Super-Admin: stat cards, paginated event table with severity badges, time-range filtering (24h/7d/30d).


Error Handling

Sanitization

  • API errors return user-friendly messages — never stack traces or internal paths
  • oRPC uses ORPCError with typed codes and i18n messages
  • Sentry captures full details server-side; clients see safe messages only
  • Session Replay remains disabled unless NEXT_PUBLIC_SENTRY_REPLAY_ENABLED=true
  • Error boundaries on route segments prevent full-page crashes

Environment Variables

Verified: No server secrets (SUPABASE_SECRET_KEY, STRIPE_SECRET_KEY, etc.) in client bundles. Only NEXT_PUBLIC_* exposed.


Key Rotation Policy

OWASP A04:2025 — Cryptographic Failures

SecretRotationOwnerNotes
SUPABASE_SECRET_KEYOn compromisePlatform super-adminDashboard → Settings → API Keys
Stripe API keyQuarterly / compromiseBilling super-adminDashboard → Developers → Roll
Creem API keyQuarterly / compromiseBilling super-adminDashboard → API Settings
Polar access tokenQuarterly / compromiseBilling super-adminDashboard → Settings → API
Dodo API keyQuarterly / compromiseBilling super-adminDashboard → API Keys
LemonSqueezy API keyQuarterly / compromiseBilling super-adminDashboard → Settings → API
Webhook secrets (×5)On compromiseBilling super-adminUpdate provider + env simultaneously
NEXT_AUTH_SECRETOn compromiseDevOpsRequires user re-authentication
CSRF_SECRETOn deploy (recommended)DevOpsNo user impact — sessions valid
GEMINI_API_KEYOn compromiseAI super-adminCloud Console → Credentials
Upstash Redis credsOn compromisePlatform super-adminUpstash Console → Database
JWT signing keyManaged by SupabaseN/AAuto-rotated internally

Rotation Procedure:

  1. Generate new secret in provider dashboard
  2. Update env var in Vercel (production + preview)
  3. Trigger deployment
  4. Verify via health check endpoint
  5. Revoke old key in provider dashboard
  6. Log rotation in activity_logs

Testing Coverage

SuiteToolFilesTestsCoverage
UnitVitest84900Actions, utilities, config, payments, auth, security, services
E2E: AuthPlaywright1—Signup, login, reset, sessions
E2E: Super-AdminPlaywright1—Users, impersonation, RBAC
E2E: BillingPlaywright1—Checkout, subscriptions, portal
E2E: CheckoutPlaywright1—Guest checkout, tier selection
E2E: OrgsPlaywright1—Org CRUD, invitations, members
E2E: AccountPlaywright1—Deletion, cleanup, session termination
E2E: MFAPlaywright1—TOTP enrollment, QR code, OTP input
E2E: PasskeysPlaywright1—WebAuthn registration, security tab UI
E2E: ExamplePlaywright1—Template for new E2E tests

CI Pipeline

  1. npm run lint — ESLint
  2. npm run type-check - Application and setup-wizard TypeScript strict gates
  3. npm run build — Production build
  4. Tests must pass before merge
  5. npm run security:audit — Production dependency scanning (A03)
  6. CycloneDX SBOM generation + GitHub Actions artifact
  7. Dependabot for automated security PRs

Compliance Readiness

FrameworkStatusEvidence
OWASP Top 10FullAll 10 categories — A01 through A10
GDPRPartialPII opt-in analytics, IP anonymization, consent tracking
SOC 2FoundationAudit trail, access controls, encryption in transit
PCI DSSDelegatedPayment processing via PCI-compliant providers

Full GDPR, SOC 2, and PCI DSS compliance requires organizational policies beyond these technical controls.


How to Use This Report

  • Enterprise sales — Share with prospects requesting security docs. Zero-critical result + RLS on 32 tables + OWASP compliance addresses procurement requirements.
  • Security teams — See THREAT_MODEL.md (STRIDE) · LLM_SECURITY.md · SUPPLY_CHAIN_SECURITY.md
  • Compliance — Maps to OWASP Top 10 categories with implementation + verification per section.
  • Developers — Run .agents/workflows/security-audit.md after architectural changes.

Copyright © 2026 Vibe44. Built with ❤️ by Scale with Alap.