Skip to content

Security: ohong/straude

Security

docs/SECURITY.md

Security Audit

Last audited: 2026-02-18

Inventory

Component Detail
Stack Next.js 16 (App Router) + Supabase (Postgres + Auth + Storage)
Client key NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY (new model, not legacy anon)
Server key SUPABASE_SECRET_KEY (env-only, never in client bundle)
Tables 9 public tables, all with RLS enabled
API routes 18 endpoints across auth, users, posts, social, upload, AI
Auth Supabase Auth (GitHub OAuth), CLI JWT (HS256)

Findings

Fixed

CRITICAL: Open redirect in auth callbackapps/web/app/(auth)/callback/route.ts:7

The next query parameter was used raw in NextResponse.redirect(). An attacker could craft ?next=//evil.com to redirect users to a phishing page after login. Now validates the parameter is a relative path that doesn't start with //.

HIGH: Missing security headersapps/web/next.config.ts

No X-Frame-Options, HSTS, X-Content-Type-Options, or Referrer-Policy. The app was frameable (clickjacking) and lacked MIME sniff protection. Added five headers to all routes:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: strict-origin-when-cross-origin
  • Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  • Permissions-Policy: camera=(), microphone=(), geolocation=()

HIGH: Mutable search_path on calculate_streaks_batch — Supabase function

Flagged by Supabase security advisor. A mutable search_path allows a malicious schema to shadow the public schema. Pinned to SET search_path = public.

MEDIUM: RLS performance on notificationsnotifications table policies

auth.uid() was re-evaluated per row instead of once per query. Replaced with (select auth.uid()) pattern per Supabase docs.

Requires Manual Action

HIGH: Leaked password protection disabled — Supabase Auth settings

Supabase can check passwords against HaveIBeenPwned to block known-compromised passwords. This is currently off. Enable in Supabase Dashboard > Authentication > Settings > Password Security.

Passed

CLI release supply chain. CLI runtime collection accepts any stable ccusage >=20.0.18, while CI installs the exact 20.0.18 graph recorded in the frozen Bun lockfile. Fresh installs may resolve a later stable release, including a new major, only to have its output pass the same strict schema, accounting, and pricing checks before submission. The scheduled/manual compatibility canary tests ccusage@latest in isolation through the production parser, and the runtime never downloads latest. PR CI builds one npm tarball and checks Ubuntu Node 20/22 plus macOS and Windows Node 22; tag releases retain all six OS/Node combinations. The tag workflow requests npm trusted-publishing credentials over OIDC, carries no long-lived publish token, and publishes only after the package matrix passes; npm must be configured to trust release-cli.yml before the first release. Source maps are excluded from npm and GitHub releases, then retained as short-lived release-workflow artifacts for production diagnosis.

RLS enabled on all 9 tables with appropriate policies. Live database confirmed:

Table Policies Write guard
users 2 (SELECT, UPDATE) id = auth.uid()
posts 4 (SELECT, INSERT, UPDATE, DELETE) user_id = auth.uid()
comments 4 (SELECT, INSERT, UPDATE, DELETE) user_id = auth.uid()
follows 3 (SELECT, INSERT, DELETE) follower_id = auth.uid()
kudos 3 (SELECT, INSERT, DELETE) user_id = auth.uid()
daily_usage 3 (SELECT, INSERT, UPDATE) user_id = auth.uid()
notifications 2 (SELECT, UPDATE) user_id = auth.uid()
cli_auth_codes 2 (SELECT, UPDATE) user_id = auth.uid()
countries_to_regions 1 (SELECT) Read-only reference table

No secrets in client-side code. All Supabase client calls use NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY (safe to expose). SUPABASE_SECRET_KEY, CLI_JWT_SECRET, and ANTHROPIC_API_KEY only appear in server-side files. No hardcoded keys found.

All write endpoints require authentication. Every POST/PATCH/DELETE API route checks supabase.auth.getUser() and returns 401 if missing. CLI endpoints validate JWT signatures with timing-safe comparison.

Input validation on all user-facing endpoints. Username regex ^[a-zA-Z0-9_]{3,20}$, comment/bio/title length limits, file type whitelist + 5MB size cap on uploads, search min length, SSRF prevention on AI caption route.

No XSS vulnerabilities. No dangerouslySetInnerHTML. User content rendered as plain text via React's default escaping. suppressHydrationWarning only on non-user-controlled timestamps.

No SQL injection. All queries use Supabase SDK parameterized methods. No raw SQL or string interpolation.

CORS configuration. Relies on Next.js defaults (restrictive). No Access-Control-Allow-Origin: * anywhere.

Accepted Risks

Materialized views accessible to anon role. Leaderboard data is intentionally public. The Supabase advisor flags leaderboard_daily, leaderboard_weekly, leaderboard_monthly, and leaderboard_all_time, but they contain only aggregated, non-sensitive data (usernames, output token counts, streaks).

comments/follows/kudos SELECT policies use USING (true). Anyone can read all comments, follow relationships, and kudos. This is intentional for a social platform where this data is public. Write policies still enforce ownership via auth.uid().

Summary

Severity Found Fixed Manual Accepted
CRITICAL 1 1 0 0
HIGH 3 2 1 0
MEDIUM 1 1 0 0
INFO 2 0 0 2

Remaining Action Items

  1. Enable leaked password protection in Supabase dashboard (HIGH)
  2. Add rate limiting to data creation endpoints — see ROADMAP.md
  3. Add Content Security Policy once all script/style sources are inventoried — see ROADMAP.md

There aren't any published security advisories