Close cross-tenant data leaks: RLS, tabular IDOR, folder scoping
From the PR description
Summary
Three fixes for paths that allowed one user to read another user's private data.
1. Enable Row Level Security on every data table (CRITICAL)
Only user_profiles had RLS. Every other table - projects, documents, document_versions, document_edits, chats, chat_messages, tabular_reviews, tabular_cells, tabular_review_chats, tabular_review_chat_messages, workflows, workflow_shares, hidden_workflows, project_subfolders - was reachable via the public anon key (NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY, shipped in the JS bundle). Anyone in the world could query PostgREST directly:
GET /rest/v1/chat_messages?select=*
GET /rest/v1/tabular_cells?select=*
...and read every chat message and every extracted document excerpt across all tenants.
The backend uses the service-role key (which bypasses RLS) and the frontend only queries user_profiles directly, so enabling RLS without policies on the data tables is sufficient - anon/authenticated are locked out, the backend keeps working.
backend/migrations/000_one_shot_schema.sql: inlineenable row level securityafter each table.backend/migrations/001_enable_rls_on_data_tables.sql: incremental migration for existing deployments.
2. Validate document access in tabular routes (CRITICAL)
POST /tabular-review, PATCH /tabular-review/:reviewId (when document_ids change), POST /tabular-review/:reviewId/regenerate-cell, and POST /tabular-review/:reviewId/generate accepted arbitrary document_ids. The user could attach any other tenant's document to a review they own and stream the LLM-extracted content back. Added filterAccessibleDocumentIds to backend/src/lib/access.ts and ensureDocAccess calls at each boundary.
3. Scope folder operations to the current project
PATCH /projects/:projectId/folders/:folderId walked parent_folder_id without eq("project_id", projectId), and PATCH /projects/:projectId/documents/:documentId/folder accepted any folder_id. Both now reject folder IDs that don't belong to the current project.
Test plan
- Apply
001_enable_rls_on_data_tables.sqlagainst a staging DB; confirm anonselectondocuments/chat_messages/tabular_cellsreturns[]while the backend continues to read+write through the service role. - As user A, create a tabular review and
POSTwithdocument_idscontaining a document owned by user B → expect403. - As user A,
PATCHan existing review adding user B'sdocument_id→ expect403. -
POST /tabular-review/:id/regenerate-cellwith a foreigndocument_id→ expect404. - As a project member of project A, attempt to
PATCHa folder'sparent_folder_idto a folder in project B → expect400. - As a project member,
PATCHa doc'sfolder_idto a folder in another project → expect400. -
npm run build --prefix backend- passes.
Not in this PR (recommended follow-ups)
- Replace
auth.admin.listUsers({ perPage: 1000 })in/projects/:id/peopleand/tabular-review/:id/peoplewith targetedgetUserByIdlookups. - Add an
expfield to download tokens (backend/src/lib/downloadTokens.ts) and remove the"dev-secret"fallback in production. - Normalize legacy uppercase entries in
projects.shared_with/tabular_reviews.shared_withso case-sensitive.includes()/@>lookups stop silently denying access.
Generated by Claude Code
Our analysis
Lock down cross-tenant data access in the Mike backend — read the full analysis →
Think the analysis missed something the PR description covers?
Capture this PR into my fork
Download a Markdown prompt that tells Claude how to port every
commit in this PR into your working tree. Run it via
claude -p < capture-pull-1.md from
inside the repo you want the changes in.