Skip to content

[codex] Harden CLI auth and durable rate limits#130

Merged
ohong merged 4 commits into
mainfrom
codex/security-sweep
Jul 3, 2026
Merged

[codex] Harden CLI auth and durable rate limits#130
ohong merged 4 commits into
mainfrom
codex/security-sweep

Conversation

@ohong

@ohong ohong commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary

This PR implements the coordinated security sweep for the CLI login flow, public database exposure, durable abuse limits, expensive request hardening, AI caption image validation, and vulnerable dependency paths.

What changed

  • Hardened CLI device auth by splitting the human code, browser-only verify secret, and CLI-only poll secret.
  • Added one-time redemption for completed CLI auth codes and removed direct public client access to public.cli_auth_codes through a Supabase migration.
  • Replaced the in-memory API limiter with a Supabase-backed api_rate_limits table and atomic check_rate_limit RPC.
  • Added durable limits for CLI init, uploads, usage submits, social actions, messages, and AI captions.
  • Capped usage submit request bodies at 256 KB, rejected duplicate dates and over-32-entry batches, and bounded per-entry processing concurrency to 4 while preserving response ordering.
  • Restricted AI caption image inputs to first-party public post-images storage URLs.
  • Updated vulnerable dependency paths for Supabase, PostHog, Vitest/Vite, Turbo, Resend, and related transitive overrides.

Impact

Existing CLI login clients that only send { code } to /api/auth/cli/poll are intentionally no longer supported. Current CLI login stores the new poll_secret in memory and includes it on every poll.

Root Cause

The previous device-login code was redeemable by code alone, and cli_auth_codes still had public client access paths. Rate limiting was also process-local, so it did not survive serverless instance churn. Several high-cost routes could do too much work before durable abuse checks or strict input caps.

Validation

  • bun install --frozen-lockfile
  • bun audit --json
  • bun run typecheck
  • bun run lint
  • bun run test
  • focused web security tests: usage submit, CLI auth, AI caption, rate limit, migration safety
  • focused CLI tests: login and sync flow
  • migration smoke-tested against local Supabase Postgres inside BEGIN ... ROLLBACK, including a check_rate_limit first-call allowed / second-call denied check
  • git diff --check

Notes

bun --cwd apps/web test:integration was not run locally because the current local Supabase migration history contains applied migration 20260602000000, but that migration file is not present in this checkout. I did not repair local migration history automatically.

Summary by CodeRabbit

  • New Features

    • Added durable, server-backed rate limiting across APIs.
    • Enforced per-user AI caption quotas and stronger caption request validation.
  • Security & Validation

    • CLI device-flow hardened with secret hashing; polling and verification now require poll_secret/verify_secret, and verification URLs include the required secret.
    • AI caption image inputs restricted to first-party Straude post uploads.
    • Usage submissions now enforce request size, entry-count limits, duplicate-date rejection, and stricter JSON parsing.
  • Bug Fixes

    • Rate-limit checks now correctly wait for the limiter response.
  • Tests / Chores

    • Expanded/adjusted coverage and updated dependencies/migrations for rate limits and CLI hardening.

@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
straude Ready Ready Preview, Comment Jul 3, 2026 11:11am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a7799348-16ce-41dc-919f-5b224e5a49f1

📥 Commits

Reviewing files that changed from the base of the PR and between 0370aa3 and 386fc3f.

📒 Files selected for processing (1)
  • supabase/migrations/20260703000000_grant_service_role_usage_tables.sql
✅ Files skipped from review due to trivial changes (1)
  • supabase/migrations/20260703000000_grant_service_role_usage_tables.sql

📝 Walkthrough

Walkthrough

This PR moves rate limiting to Supabase RPC, adds CLI device secrets for auth verification and polling, tightens AI caption and usage submission request validation, and updates related tests, client flows, migrations, and package pins.

Changes

Durable Rate Limiting & CLI Auth Hardening

Layer / File(s) Summary
Migration foundation for rate limits and CLI auth
supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql, apps/web/__tests__/unit/migration-safety.test.ts
Adds api_rate_limits, check_rate_limit, new cli_auth_codes secret/redemption columns, and SQL assertions for the hardened migration.
Async Supabase RPC rate-limit library
apps/web/lib/rate-limit.ts, apps/web/__tests__/unit/rate-limit.test.ts
Replaces the in-memory limiter with async RPC-backed checks and covers allow/deny/error responses.
CLI secret helpers and auth route enforcement
apps/web/lib/api/cli-auth.ts, apps/web/app/api/auth/cli/init/route.ts, apps/web/app/api/auth/cli/poll/route.ts, apps/web/app/api/auth/cli/verify/route.ts
Adds device-secret generation/hashing and threads poll_secret/verify_secret through init, poll, and verify handlers.
CLI client and web verify surface updates
apps/web/app/cli/verify/page.tsx, packages/cli/src/commands/login.ts, apps/web/e2e/golden-path/cli-verify.spec.ts
Propagates poll_secret and verify_secret through the web verify page, CLI login command, and e2e coverage.
Async rateLimit callsite adoption and shared RPC test mocks
apps/web/app/api/comments/[id]/reactions/route.ts, apps/web/app/api/comments/[id]/route.ts, apps/web/app/api/follow/[username]/route.ts, apps/web/app/api/messages/route.ts, apps/web/app/api/posts/[id]/comments/route.ts, apps/web/app/api/posts/[id]/kudos/route.ts, apps/web/app/api/upload/route.ts, apps/web/__tests__/api/*.test.ts, apps/web/__tests__/flows/*.test.ts
Awaited rate-limit checks at callsites and updated API/flow tests to mock service-client RPC behavior.
AI caption input policy and quota enforcement
apps/web/app/api/ai/generate-caption/route.ts, apps/web/__tests__/api/ai-caption.test.ts
Adds guarded JSON parsing, first-party post-images URL checks, durable quota enforcement, and matching tests.
Usage submit body limits and bounded concurrency
apps/web/app/api/usage/submit/route.ts, apps/web/__tests__/api/usage-submit.test.ts
Adds body-size and entry-count limits, duplicate-date validation, bounded per-entry concurrency, and expanded validation tests.
Workspace dependency and override updates
apps/web/package.json, packages/cli/package.json, package.json
Pins and bumps runtime/dev dependencies and expands root overrides.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • ohong/straude#125: Overlaps the usage submission route and its tests with related request-handling changes.
  • ohong/straude#114: Shares the CLI login and activation flow surface, including client-side auth handling.
  • ohong/straude#92: Also touches the usage submission path and adjacent validation/processing logic.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: CLI auth hardening and durable, Supabase-backed rate limiting.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/security-sweep

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/app/api/usage/submit/route.ts (1)

466-483: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate each entries[] item before reading date and data.

body.entries is still untrusted JSON here, but the loop dereferences entry.date and entry.data immediately. Payloads like {"entries":[null]} or {"entries":[{"date":"2026-06-09"}]} will throw and turn a bad request into a 500 instead of a 400. Add a shape check before calling isValidDate() / validateEntry().

Suggested fix
   const seenDates = new Set<string>();
   for (const entry of body.entries) {
+    if (
+      !entry
+      || typeof entry !== "object"
+      || typeof entry.date !== "string"
+      || !entry.data
+      || typeof entry.data !== "object"
+    ) {
+      return NextResponse.json(
+        { error: "Each entry must include a date and data object" },
+        { status: 400 },
+      );
+    }
+
     if (!isValidDate(entry.date)) {
       return NextResponse.json({ error: `Invalid date: ${entry.date}` }, { status: 400 });
     }
     if (seenDates.has(entry.date)) {
       return NextResponse.json({ error: `Duplicate date: ${entry.date}` }, { status: 400 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/usage/submit/route.ts` around lines 466 - 483, The loop over
body.entries dereferences entry.date and entry.data without validating entry's
shape; add a guard at the top of the loop to ensure each entry is a non-null
object with a string date and a defined data field (e.g., typeof entry ===
'object' && entry !== null && typeof entry.date === 'string' && 'data' in entry)
and return a 400 NextResponse.json error when the shape is invalid before
calling isValidDate, isWithinBackfillWindow, or validateEntry; keep using the
existing symbols seenDates, isValidDate, isWithinBackfillWindow, validateEntry
and preserve the same error-response pattern.
apps/web/e2e/golden-path/cli-verify.spec.ts (1)

45-52: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This test no longer exercises the redirect contract.

Lines 45-52 still look for an anchor, but apps/web/app/cli/verify/page.tsx now renders a button that navigates with router.push. isVisible stays false, so the assertions are skipped and the test passes even if next is broken. Click the button and assert the resulting /login?next=... URL instead.

Suggested fix
-    const signInLink = page.locator('a:has-text("Sign in to authorize")');
-    const isVisible = await signInLink.isVisible().catch(() => false);
+    const signInButton = page.getByRole("button", { name: "Sign in to authorize" });
+    const isVisible = await signInButton.isVisible().catch(() => false);

     if (isVisible) {
-      const href = await signInLink.getAttribute("href");
-      expect(href).toContain("/login");
-      expect(href).toContain("next=");
+      await signInButton.click();
+      await expect(page).toHaveURL(/\/login\?next=/);
+      await expect(page).toHaveURL(/verify_secret=/);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/e2e/golden-path/cli-verify.spec.ts` around lines 45 - 52, The test
currently checks for an anchor but the component in cli/verify/page.tsx now
renders a button that navigates with router.push, so update the test to click
the button and assert the resulting URL includes the redirect query;
specifically replace the signInLink anchor checks with a locate-and-click of the
button text "Sign in to authorize" (use the same text locator), then wait for
navigation and assert that page.url() contains "/login?next=" to verify the
redirect contract.
🧹 Nitpick comments (1)
apps/web/__tests__/unit/migration-safety.test.ts (1)

186-202: 🏗️ Heavy lift

This only proves the SQL text, not that the migration can run.

These assertions still pass if the migration is unrunnable because of statement ordering or constraint validation against legacy rows. Add one execution-based fixture that seeds an old cli_auth_codes row and applies this migration inside a transaction so deploy-time failures are caught here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/__tests__/unit/migration-safety.test.ts` around lines 186 - 202, The
current test only inspects SQL text; add an execution-based sub-test inside the
"latest cli_auth_codes hardening removes public grants and pending-code select
policies" test that uses getLatestMigrationMatching to get the migration
content, begins a DB transaction, seeds a legacy public.cli_auth_codes row (with
columns needed to trigger constraint/policy checks), applies the migration SQL
(execute the content against the test DB) inside that transaction, and then
asserts the migration ran without throwing and that the resulting
grants/policies are as expected; ensure you use the same migrations variable and
the migration content from getLatestMigrationMatching and roll back the
transaction after the assertions to keep the fixture isolated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/api/ai/generate-caption/route.ts`:
- Around line 26-34: The handler assumes request.json() returns an object and
destructures images and usage directly which throws for non-object JSON (null,
number, string); update the logic around request.json() in route.ts to validate
that the parsed body is a plain object before destructuring: call await
request.json() into body, check typeof body === "object" && body !== null (or
use Array.isArray/body instanceof Object as needed), and if the check fails
return a 400 JSON error; then safely extract images and usage from the validated
body.

In `@apps/web/app/api/auth/cli/init/route.ts`:
- Around line 46-48: The current construction of appUrl uses NEXT_PUBLIC_APP_URL
with a hardcoded production fallback which can cause incorrect verify links;
modify the logic that computes appUrl (the code that sets appUrl and builds
verifyUrl) to use request.nextUrl.origin as the fallback when
NEXT_PUBLIC_APP_URL is unset (or alternatively throw an explicit error if an app
URL must be provided), then rebuild verifyUrl with that origin and the existing
URLSearchParams (code and verify_secret) so verifyUrl points to the actual
request origin rather than "https://straude.com".

In `@apps/web/app/cli/verify/page.tsx`:
- Around line 36-39: The conditional that checks (!code || !verifySecret) in the
page component currently returns a message that only mentions a missing
authorization code; update the user-facing message to a generic "invalid or
incomplete authorization link" (or similar) so it correctly covers both missing
code and missing verifySecret. Locate the check around the variables code and
verifySecret in page.tsx and replace the text inside the <p> element rendered
for that branch to reflect the generic message.

In `@supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql`:
- Around line 5-30: The migration updates on table cli_auth_codes are ordered
such that status is changed to 'expired' before the cli_auth_codes_status_check
is widened and before new columns poll_secret_hash and verify_secret_hash exist,
which can fail; reorder operations: first ALTER TABLE to ADD COLUMN IF NOT
EXISTS poll_secret_hash, verify_secret_hash, redeemed_at, then DROP and re-ADD
the cli_auth_codes_status_check to include 'expired', then backfill or UPDATE
legacy rows (e.g., set status = 'expired' or populate secret hashes) so no rows
violate the new logic, and only after that DROP/ADD the
cli_auth_codes_active_secrets_check to enforce the stricter secret-nonnull
constraint.
- Around line 87-88: The unconditional table-wide prune using "DELETE FROM
public.api_rate_limits WHERE expires_at < v_now - interval '1 hour';" must be
removed from the request path; instead either move this cleanup to an
out-of-band job (cron/pg_cron/worker) that runs periodically for
public.api_rate_limits, or make the prune opportunistic inside the RPC by
replacing the global DELETE with a bounded/conditional operation (e.g. DELETE
... WHERE expires_at < v_now - interval '1 hour' ORDER BY expires_at LIMIT <N>
or DELETE only for the caller's bucket ids), ensuring you reference the
expires_at/v_now logic and avoid full-table writes from the hot RPC.

---

Outside diff comments:
In `@apps/web/app/api/usage/submit/route.ts`:
- Around line 466-483: The loop over body.entries dereferences entry.date and
entry.data without validating entry's shape; add a guard at the top of the loop
to ensure each entry is a non-null object with a string date and a defined data
field (e.g., typeof entry === 'object' && entry !== null && typeof entry.date
=== 'string' && 'data' in entry) and return a 400 NextResponse.json error when
the shape is invalid before calling isValidDate, isWithinBackfillWindow, or
validateEntry; keep using the existing symbols seenDates, isValidDate,
isWithinBackfillWindow, validateEntry and preserve the same error-response
pattern.

In `@apps/web/e2e/golden-path/cli-verify.spec.ts`:
- Around line 45-52: The test currently checks for an anchor but the component
in cli/verify/page.tsx now renders a button that navigates with router.push, so
update the test to click the button and assert the resulting URL includes the
redirect query; specifically replace the signInLink anchor checks with a
locate-and-click of the button text "Sign in to authorize" (use the same text
locator), then wait for navigation and assert that page.url() contains
"/login?next=" to verify the redirect contract.

---

Nitpick comments:
In `@apps/web/__tests__/unit/migration-safety.test.ts`:
- Around line 186-202: The current test only inspects SQL text; add an
execution-based sub-test inside the "latest cli_auth_codes hardening removes
public grants and pending-code select policies" test that uses
getLatestMigrationMatching to get the migration content, begins a DB
transaction, seeds a legacy public.cli_auth_codes row (with columns needed to
trigger constraint/policy checks), applies the migration SQL (execute the
content against the test DB) inside that transaction, and then asserts the
migration ran without throwing and that the resulting grants/policies are as
expected; ensure you use the same migrations variable and the migration content
from getLatestMigrationMatching and roll back the transaction after the
assertions to keep the fixture isolated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3449a9e-51b5-4e86-a06b-6d556e7d316a

📥 Commits

Reviewing files that changed from the base of the PR and between f88fdf7 and 89143f6.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (34)
  • apps/web/__tests__/api/ai-caption.test.ts
  • apps/web/__tests__/api/auth-cli.test.ts
  • apps/web/__tests__/api/comment-email-notifications.test.ts
  • apps/web/__tests__/api/social.test.ts
  • apps/web/__tests__/api/upload.test.ts
  • apps/web/__tests__/api/usage-submit.test.ts
  • apps/web/__tests__/flows/cli-push-flow.test.ts
  • apps/web/__tests__/flows/post-lifecycle.test.ts
  • apps/web/__tests__/flows/social-interactions.test.ts
  • apps/web/__tests__/flows/web-import-flow.test.ts
  • apps/web/__tests__/unit/migration-safety.test.ts
  • apps/web/__tests__/unit/rate-limit.test.ts
  • apps/web/app/api/ai/generate-caption/route.ts
  • apps/web/app/api/auth/cli/init/route.ts
  • apps/web/app/api/auth/cli/poll/route.ts
  • apps/web/app/api/auth/cli/verify/route.ts
  • apps/web/app/api/comments/[id]/reactions/route.ts
  • apps/web/app/api/comments/[id]/route.ts
  • apps/web/app/api/follow/[username]/route.ts
  • apps/web/app/api/messages/route.ts
  • apps/web/app/api/posts/[id]/comments/route.ts
  • apps/web/app/api/posts/[id]/kudos/route.ts
  • apps/web/app/api/upload/route.ts
  • apps/web/app/api/usage/submit/route.ts
  • apps/web/app/cli/verify/page.tsx
  • apps/web/e2e/golden-path/cli-verify.spec.ts
  • apps/web/lib/api/cli-auth.ts
  • apps/web/lib/rate-limit.ts
  • apps/web/package.json
  • package.json
  • packages/cli/__tests__/commands/login.test.ts
  • packages/cli/package.json
  • packages/cli/src/commands/login.ts
  • supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql

Comment on lines +26 to 34
let body: { images?: unknown; usage?: CaptionUsage };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}

const { images, usage } = body;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard non-object JSON payloads before destructuring.

request.json() can return valid non-object JSON (null, number, string). Destructuring body then throws and turns a client error into a 500.

Suggested fix
-  let body: { images?: unknown; usage?: CaptionUsage };
+  let body: unknown;
   try {
     body = await request.json();
   } catch {
     return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
   }
 
-  const { images, usage } = body;
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
+  }
+  const { images, usage } = body as { images?: unknown; usage?: CaptionUsage };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/ai/generate-caption/route.ts` around lines 26 - 34, The
handler assumes request.json() returns an object and destructures images and
usage directly which throws for non-object JSON (null, number, string); update
the logic around request.json() in route.ts to validate that the parsed body is
a plain object before destructuring: call await request.json() into body, check
typeof body === "object" && body !== null (or use Array.isArray/body instanceof
Object as needed), and if the check fails return a 400 JSON error; then safely
extract images and usage from the validated body.

Comment on lines +46 to +48
const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? "https://straude.com").replace(/\/+$/, "");
const params = new URLSearchParams({ code, verify_secret: verifySecret });
const verifyUrl = `${appUrl}/cli/verify?${params.toString()}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Derive verify_url from the current request origin instead of a production fallback.

Line 46 falls back to https://straude.com when NEXT_PUBLIC_APP_URL is unset. In any staging or self-hosted deployment, packages/cli/src/commands/login.ts will open the browser on the wrong origin and send code plus verify_secret there. Use request.nextUrl.origin as the fallback, or fail fast if the app URL is required.

Suggested fix
-  const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? "https://straude.com").replace(/\/+$/, "");
+  const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? request.nextUrl.origin).replace(/\/+$/, "");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/auth/cli/init/route.ts` around lines 46 - 48, The current
construction of appUrl uses NEXT_PUBLIC_APP_URL with a hardcoded production
fallback which can cause incorrect verify links; modify the logic that computes
appUrl (the code that sets appUrl and builds verifyUrl) to use
request.nextUrl.origin as the fallback when NEXT_PUBLIC_APP_URL is unset (or
alternatively throw an explicit error if an app URL must be provided), then
rebuild verifyUrl with that origin and the existing URLSearchParams (code and
verify_secret) so verifyUrl points to the actual request origin rather than
"https://straude.com".

Comment thread apps/web/app/cli/verify/page.tsx
Comment on lines +5 to +30
UPDATE public.cli_auth_codes
SET status = 'expired'
WHERE status IN ('pending', 'completed')
AND expires_at > now();

ALTER TABLE public.cli_auth_codes
ADD COLUMN IF NOT EXISTS poll_secret_hash text,
ADD COLUMN IF NOT EXISTS verify_secret_hash text,
ADD COLUMN IF NOT EXISTS redeemed_at timestamptz;

ALTER TABLE public.cli_auth_codes
DROP CONSTRAINT IF EXISTS cli_auth_codes_status_check;

ALTER TABLE public.cli_auth_codes
ADD CONSTRAINT cli_auth_codes_status_check
CHECK (status IN ('pending', 'completed', 'expired', 'used'));

ALTER TABLE public.cli_auth_codes
DROP CONSTRAINT IF EXISTS cli_auth_codes_active_secrets_check;

ALTER TABLE public.cli_auth_codes
ADD CONSTRAINT cli_auth_codes_active_secrets_check
CHECK (
status = 'expired'
OR (poll_secret_hash IS NOT NULL AND verify_secret_hash IS NOT NULL)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Backfill legacy cli_auth_codes rows before validating the new checks.

This sequence can fail on existing data. status = 'expired' is written before cli_auth_codes_status_check is widened to allow expired, and the later cli_auth_codes_active_secrets_check will still reject any legacy pending / completed / used row that has null secret hashes. Reorder this so the new columns exist first, the status check is widened, all pre-secret rows are marked terminal or backfilled, and only then is the stricter secrets check validated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql`
around lines 5 - 30, The migration updates on table cli_auth_codes are ordered
such that status is changed to 'expired' before the cli_auth_codes_status_check
is widened and before new columns poll_secret_hash and verify_secret_hash exist,
which can fail; reorder operations: first ALTER TABLE to ADD COLUMN IF NOT
EXISTS poll_secret_hash, verify_secret_hash, redeemed_at, then DROP and re-ADD
the cli_auth_codes_status_check to include 'expired', then backfill or UPDATE
legacy rows (e.g., set status = 'expired' or populate secret hashes) so no rows
violate the new logic, and only after that DROP/ADD the
cli_auth_codes_active_secrets_check to enforce the stricter secret-nonnull
constraint.

Comment on lines +87 to +88
DELETE FROM public.api_rate_limits
WHERE expires_at < v_now - interval '1 hour';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid global row pruning on every rate-limit check.

Every RPC call does a table-wide DELETE before touching the caller's bucket. Since this function now fronts multiple hot endpoints, that turns rate limiting into shared cleanup work and will create unnecessary write/VACUUM churn under load. Move pruning out of the request path or make it opportunistic instead of unconditional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql`
around lines 87 - 88, The unconditional table-wide prune using "DELETE FROM
public.api_rate_limits WHERE expires_at < v_now - interval '1 hour';" must be
removed from the request path; instead either move this cleanup to an
out-of-band job (cron/pg_cron/worker) that runs periodically for
public.api_rate_limits, or make the prune opportunistic inside the RPC by
replacing the global DELETE with a bounded/conditional operation (e.g. DELETE
... WHERE expires_at < v_now - interval '1 hour' ORDER BY expires_at LIMIT <N>
or DELETE only for the caller's bucket ids), ensuring you reference the
expires_at/v_now logic and avoid full-table writes from the hot RPC.

The service client (POST /api/usage/submit) writes daily_usage, device_usage,
and posts, but those tables were only GRANTed to `authenticated` — service_role
relied on Postgres default privileges. Newer local Supabase images enforce
table GRANTs for service_role, so a freshly-booted `supabase start` stack
returns 'permission denied for table device_usage', failing the real-Supabase
integration test on every PR. Hosted Supabase already has these grants, so
this is a no-op there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ohong ohong merged commit 2de5197 into main Jul 3, 2026
5 checks passed
@ohong ohong deleted the codex/security-sweep branch July 3, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant