Implement the following plan:
Medium security fixes from #149
Eight items, one PR each, ordered by simplicity and independence.
1. SQLite database world-readable
File: cli/src/core/event-store.ts:55-71
The .traces directory is created with fs.mkdirSync(baseDir, { recursive: true }) without a mode. The database file gets default permissions too. On multiuser systems, other users could read trace data.
Fix:
fs.mkdirSync(baseDir, { recursive: true, mode: 0o700 }) (owner-only)
- After opening the database,
fs.chmodSync(dbPath, 0o600) on the db file
- Same treatment in
cli/src/core/auth-config.ts:89 (noted in low severity but same pattern)
Test: Unit test that creates a store in a temp dir and checks permissions.
2. Open redirect on login
File: frontend/app/login/success/page.tsx:24
router.replace(redirectTo || "/") uses the raw redirectTo query param. An attacker can craft /login/success?redirectTo=https://evil.com.
The API layer already validates redirect origins in api/convex/http/v1/oauth.ts via resolveRedirectUrl, but the frontend success page doesn't.
Fix: Validate redirectTo is a relative path (starts with /, doesn't start with //). Fall back to / otherwise.
typescript
3. Token embedded in git clone URL
File: .github/workflows/release-cli.yml:164
git clone https://x-access-token:${GH_TOKEN}@github.com/... embeds the token in the command, which could leak through git error output or process listing.
Fix: Clone without credentials, then set the push URL with the token afterward, or use gh repo clone.
yaml
4. No security headers on frontend
File: frontend/next.config.ts
No security headers configured at all.
Fix: Add headers() to next config:
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Skip CSP for now as it requires auditing all inline scripts/styles and would be a large separate effort.
5. reset:clearAll is a public Convex action
File: api/convex/reset.ts:88
Exported as a public action, guarded only by env var heuristics. If env vars are misconfigured, anyone can wipe all data.
Fix: Change from action() to internalAction(). The reset script and CLI that calls it use npx convex run which can invoke internal functions. No external client should ever call this.
6. Feed queries scan private traces
File: api/convex/internal/feed.ts:20-23
typescript
Full table scan, private traces fetched then filtered in memory.
Fix:
- Add
by_visibility index to api/convex/schema.ts: .index("by_visibility", ["visibility"])
- Refactor query to use
withIndex("by_visibility", q => q.eq("visibility", "public"))
- Same for
_listPublicTraces at lines 82-88
7. Vercel preview CORS allowlist too broad
File: api/convex/http/shared.ts:28-36
Any traces-frontend-*.vercel.app hostname is trusted for credentialed CORS. An attacker could deploy a malicious Vercel app matching this pattern.
Fix: Tighten to match only the exact Vercel preview URL format: traces-frontend-<git-hash>-<team>.vercel.app. The team slug is known and can be checked. Alternatively, use an env var TRACES_VERCEL_TEAM and validate the suffix matches -<team>.vercel.app.
8. SameSite=None on session cookies
File: api/convex/lib/auth_helpers.ts:98
Production cookies use SameSite=None because the API and frontend are on different domains (the API is on a Convex self-hosted URL). This is needed for cross-origin cookie sending.
Combined with the broad CORS allowlist (#7), this creates a CSRF risk. Fixing #7 reduces the attack surface. Beyond that, the existing CSRF token mechanism provides protection as long as the origin check is tight.
Fix: After fixing #7, this becomes lower risk. Change SameSite=None to SameSite=Lax if the API and frontend share the same domain. If they don't (self-hosted Convex), SameSite=None is required and the CSRF token + tightened CORS is the defense. Add a comment documenting the security rationale.
Order of implementation
- SQLite permissions (CLI only, no deps)
- Open redirect (frontend only, no deps)
- Token in clone URL (workflow only, no deps)
- Security headers (frontend only, no deps)
- reset:clearAll internal (API only, no deps)
- Feed visibility index (API schema + query)
- CORS allowlist (API, fixes #8's risk too)
- SameSite cookies (API, depends on #7 assessment)
Verification
Each PR: run relevant test suite (bun test in the affected package), verify CI passes.
If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/andrew/.claude/projects/-Users-andrew-code-traces-traces/df9d0c2f-e266-40b8-9e1a-dcd864c4d749.jsonl