Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(security): enforce project membership across tRPC procedures (IDOR) - #3127

Open
Mohak-Agrawal wants to merge 1 commit into
onlook-dev:mainfrom
Mohak-Agrawal:fix/idor-project-scoped-trpc-procedures
Open

fix(security): enforce project membership across tRPC procedures (IDOR)#3127
Mohak-Agrawal wants to merge 1 commit into
onlook-dev:mainfrom
Mohak-Agrawal:fix/idor-project-scoped-trpc-procedures

Conversation

@Mohak-Agrawal

@Mohak-Agrawal Mohak-Agrawal commented Jul 13, 2026

Copy link
Copy Markdown

Summary

Closes #3122.

The Drizzle client connects via a Postgres superuser role that is exempt from Supabase RLS, so authorization has to be enforced in tRPC procedure code. A verifyProjectAccess() helper already existed (project/helper.ts) and was used correctly in a few procedures (project.update, project.delete), but was missing from the large majority of project-scoped procedures — any authenticated user could read, modify, or delete data belonging to other users' projects by supplying an arbitrary projectId/conversationId/branchId/etc.

I audited every router under server/api/routers/{project,chat} (not just the 15 procedures the original report named) and fixed everything I found unprotected — 25 procedures across 9 files:

  • project.ts: get, getProjectWithCanvas, getPreviewProjects (now pins to the caller's own session id rather than trusting a client-supplied userId)
  • fork.ts: fork — clones a source project's entire contents (canvas, branches, live sandboxes) into a new project the caller owns; not in the original report, found while auditing this router
  • member.ts: list, remove
  • invitation.ts: list, delete, create (create also unprotected, not in the original report — lets any user invite arbitrary emails into any project)
  • chat/conversation.ts: getAll, get, upsert, update, delete
  • chat/message.ts: getAll, upsert, upsertMany, update, updateCheckpoints, delete, replaceConversationMessages (this one also had a secondary issue: it deletes-then-inserts using input.conversationId for the delete but each message's own embedded conversationId for the insert — a caller authorized for conversation A could smuggle messages into conversation B via the array. Now forces every inserted message onto the authorized input.conversationId server-side.)
  • branch.ts: getByProjectId, create, update, delete, fork, createBlank
  • settings.ts: get, upsert, delete
  • frame.ts: get, getByCanvas, create, update, delete
  • createRequest.ts: getPendingRequest, updateStatus

Approach

Added resolve-then-verify helpers to project/helper.ts for resources that don't carry a projectId directly, each following the exact pattern the existing verifyProjectAccess already uses (single query, merged "Unauthorized or not found" error so the check itself can't be used to enumerate resource existence):

  • verifyConversationAccess — conversation → projectId
  • verifyMessagesAccess — message(s) → conversation → projectId (accepts an array for bulk ops like delete, verifies every resolved project)
  • verifyBranchAccess — branch → projectId
  • verifyCanvasAccess — canvas → projectId
  • verifyFrameAccess — frame → canvas → projectId
  • verifyInvitationAccess — invitation → projectId

Every mutation/query call site gets one await verify*Access(ctx.db, ctx.user.id, ...) at the top, before touching the DB. No new abstractions beyond that — same helper file, same error shape, same call convention already established in the codebase.

Not covered here

project/sandbox.ts operates on raw CodeSandbox provider ids (start/hibernate/delete/fork), not projectId-keyed DB records — it needs a different resolution path (sandboxIdbranches.sandboxIdprojectId) and felt like a separate enough concern to not fold into this diff. Happy to follow up with that in a second PR if useful.

Test plan

  • tsc --noEmit run per changed file (isolated, without bun install — this repo's bun.lock is large and I didn't want to pull the whole dependency tree for a security-fix diff). Every error that surfaced traced back to unresolvable @onlook/db imports in that isolated setup; I confirmed the specific patterns used (e.g. the Set-dedup + type-guard in verifyMessagesAccess's caller) compile clean under --strict with equivalent real types in isolation.
  • Would appreciate a maintainer running the full bun run typecheck + bun test suite against this branch, and ideally exercising the original repro from Cross-user IDOR across multiple tRPC procedures #3122 against it, since I don't have a local Supabase stack set up to verify end-to-end myself.
  • Read through every touched procedure to confirm the added check doesn't change behavior for an authorized caller — only rejects callers who aren't a member of the resolved project.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security
    • Strengthened access controls across projects, conversations, messages, branches, canvases, frames, invitations, members, and settings.
    • Prevented unauthorized reads, updates, deletions, and creation of project-related content.
    • Added validation to prevent messages from being inserted into the wrong conversation.
    • Ensured project previews can only be requested for the authenticated account.
    • Standardized unauthorized or missing-resource responses.

Closes onlook-dev#3122.

The Drizzle ORM client connects via a Postgres superuser role that is
exempt from Supabase RLS, so authorization must be enforced in tRPC
procedure code. A verifyProjectAccess() helper already existed and was
used correctly in some procedures (project.update, project.delete), but
was missing from the large majority -- any authenticated user could
read, modify, or delete data belonging to other users' projects by
supplying an arbitrary projectId/conversationId/branchId/etc.

Adds resolve-then-verify helpers to project/helper.ts for resources
that don't carry a projectId directly (verifyConversationAccess,
verifyMessagesAccess, verifyBranchAccess, verifyCanvasAccess,
verifyFrameAccess, verifyInvitationAccess), each following the existing
verifyProjectAccess pattern: a merged "Unauthorized or not found" error
so the check can't be used to enumerate resource existence.

Wires these into every procedure that reads or writes project-scoped
data and was missing a check:

- project.ts: get, getProjectWithCanvas, getPreviewProjects (now pins
  to the caller's own session id instead of a client-supplied userId)
- fork.ts: fork -- clones a source project's full contents (canvas,
  branches, live sandboxes); not in the original report but the same
  class of bug, found while auditing this router
- member.ts: list, remove
- invitation.ts: list, delete, create (create also unprotected, not in
  the original report)
- chat/conversation.ts: getAll, get, upsert, update, delete
- chat/message.ts: getAll, upsert, upsertMany, update,
  updateCheckpoints, delete, replaceConversationMessages (also forces
  the inserted conversationId server-side so a caller authorized for
  conversation A can't smuggle messages into conversation B via the
  payload array)
- branch.ts: getByProjectId, create, update, delete, fork, createBlank
- settings.ts: get, upsert, delete
- frame.ts: get, getByCanvas, create, update, delete
- createRequest.ts: getPendingRequest, updateStatus

25 procedures across 9 files; the original report named 15 across 5.

Not covered here: project/sandbox.ts operates on raw CodeSandbox
provider ids (start/hibernate/delete/fork), not projectId-keyed DB
records, and needs a different resolution path (sandboxId ->
branches.sandboxId -> projectId). Flagging as a likely follow-up rather
than folding it into this diff.

Verified with `tsc --noEmit` per file (bun install wasn't run given the
monorepo's size and this being a security-fix diff, not a build check);
every flagged type error traced back to unresolvable @onlook/db imports
in that isolated environment, confirmed against an equivalent pattern
with real types compiling clean under --strict.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs-onlook Skipped Skipped Jul 13, 2026 3:42pm

Request Review

@vercel
vercel Bot temporarily deployed to Preview – docs-onlook July 13, 2026 15:42 Inactive
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

@Mohak-Agrawal is attempting to deploy a commit to the Onlook Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Project-, conversation-, message-, branch-, canvas-, frame-, invitation-, and member-scoped procedures now perform explicit authorization checks through shared helpers. Conversation replacement also binds inserted messages to the requested conversation, and preview-project access validates the authenticated user ID.

Changes

Authorization enforcement

Layer / File(s) Summary
Shared access helpers
apps/web/client/src/server/api/routers/project/helper.ts
Adds helpers that validate entities, resolve parent projects, and authorize collections of messages.
Chat authorization gates
apps/web/client/src/server/api/routers/chat/conversation.ts, apps/web/client/src/server/api/routers/chat/message.ts
Adds project, conversation, and message checks across chat reads and mutations; replacement inserts use the requested conversation ID.
Branch and frame authorization
apps/web/client/src/server/api/routers/project/branch.ts, apps/web/client/src/server/api/routers/project/frame.ts
Adds access checks before branch, sandbox, canvas, and frame operations.
Collaboration authorization
apps/web/client/src/server/api/routers/project/createRequest.ts, apps/web/client/src/server/api/routers/project/invitation.ts, apps/web/client/src/server/api/routers/project/member.ts
Adds project or invitation checks before request, invitation, and membership operations.
Project and settings authorization
apps/web/client/src/server/api/routers/project/project.ts, apps/web/client/src/server/api/routers/project/fork.ts, apps/web/client/src/server/api/routers/project/settings.ts
Adds project checks to project reads, forks, and settings operations, plus authenticated-user validation for preview projects.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Router
  participant AccessHelper
  participant Database
  Client->>Router: invoke protected procedure
  Router->>AccessHelper: verify scoped access
  AccessHelper->>Database: query entity and project membership
  AccessHelper-->>Router: allow or throw unauthorized
  Router->>Database: perform authorized operation
  Database-->>Client: return procedure result
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: kitenite

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: enforcing project membership checks to fix an IDOR/security issue.
Description check ✅ Passed The description covers the problem, scope, approach, and test plan, though it doesn't follow the template headings exactly and omits some optional sections.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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: 3

Caution

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

⚠️ Outside diff range comments (2)
apps/web/client/src/server/api/routers/chat/message.ts (1)

27-46: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Scope the upsert to the existing conversation conversationId is required, but the conflict path still trusts the client-supplied id, so a caller who knows another message ID can overwrite that row and reassign it to the request conversation. Add a same-conversation check before onConflictDoUpdate, or avoid conflict-based updates 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/client/src/server/api/routers/chat/message.ts` around lines 27 - 46,
The upsert mutation currently allows a client-supplied message ID to update a
message from another conversation. In the upsert procedure, validate that any
existing message identified by normalizedMessage.id belongs to the requested
conversationId before onConflictDoUpdate, rejecting mismatches; preserve inserts
and same-conversation updates.
apps/web/client/src/server/api/routers/project/invitation.ts (1)

24-51: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add verifyProjectAccess to suggested

  • suggested accepts a projectId but never checks access, so any authenticated user can enumerate email addresses for arbitrary projects.
🤖 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/client/src/server/api/routers/project/invitation.ts` around lines 24
- 51, Add verifyProjectAccess to the suggested procedure before querying or
returning invitation email data, using its supplied projectId and request
context. Preserve the existing suggested behavior for authorized users while
rejecting access to projects the authenticated user cannot access.
🧹 Nitpick comments (1)
apps/web/client/src/server/api/routers/chat/conversation.ts (1)

14-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use path aliases instead of relative imports for helper module.

Both new imports use '../project/helper' instead of the @/* or ~/* path aliases required by the coding guidelines for files under apps/web/client/src/**.

  • apps/web/client/src/server/api/routers/chat/conversation.ts#L14-L14: Change from '../project/helper' to from '@/server/api/routers/project/helper' (or ~/...).
  • apps/web/client/src/server/api/routers/chat/message.ts#L12-L12: Same change.

As per coding guidelines: "Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*".

🤖 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/client/src/server/api/routers/chat/conversation.ts` at line 14,
Replace the relative helper imports in
apps/web/client/src/server/api/routers/chat/conversation.ts:14 and
apps/web/client/src/server/api/routers/chat/message.ts:12 with the `@/`* or ~/*
alias targeting the project helper module, preserving the existing
verifyConversationAccess and verifyProjectAccess imports.

Source: Coding guidelines

🤖 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/client/src/server/api/routers/project/branch.ts`:
- Line 50: Move the authorization checks outside the broad try-catch blocks so
authorization errors propagate to clients instead of being converted to false.
In apps/web/client/src/server/api/routers/project/branch.ts lines 50, 60, and
81, update create to call verifyProjectAccess before its try-catch and
update/delete to call verifyBranchAccess before their try-catches; in
apps/web/client/src/server/api/routers/project/frame.ts lines 42, 54, and 75,
likewise move verifyCanvasAccess or verifyFrameAccess before the corresponding
create, update, and delete try-catches.

In `@apps/web/client/src/server/api/routers/project/helper.ts`:
- Around line 95-104: Deduplicate messageIds before the findMany query and
authorization count check so repeated IDs do not cause a false mismatch. Update
the logic around messageIds and rows.length to compare against the unique ID
count while preserving the existing empty-input return and
unauthorized/not-found error behavior.

In `@apps/web/client/src/server/api/routers/project/project.ts`:
- Around line 363-370: Update getPreviewProjects to throw the file’s established
TRPCError type with the appropriate unauthorized code instead of a plain Error,
and use ctx.user.id rather than input.userId in the userProjects query filter.
Preserve the existing identity validation before querying.

---

Outside diff comments:
In `@apps/web/client/src/server/api/routers/chat/message.ts`:
- Around line 27-46: The upsert mutation currently allows a client-supplied
message ID to update a message from another conversation. In the upsert
procedure, validate that any existing message identified by normalizedMessage.id
belongs to the requested conversationId before onConflictDoUpdate, rejecting
mismatches; preserve inserts and same-conversation updates.

In `@apps/web/client/src/server/api/routers/project/invitation.ts`:
- Around line 24-51: Add verifyProjectAccess to the suggested procedure before
querying or returning invitation email data, using its supplied projectId and
request context. Preserve the existing suggested behavior for authorized users
while rejecting access to projects the authenticated user cannot access.

---

Nitpick comments:
In `@apps/web/client/src/server/api/routers/chat/conversation.ts`:
- Line 14: Replace the relative helper imports in
apps/web/client/src/server/api/routers/chat/conversation.ts:14 and
apps/web/client/src/server/api/routers/chat/message.ts:12 with the `@/`* or ~/*
alias targeting the project helper module, preserving the existing
verifyConversationAccess and verifyProjectAccess imports.
🪄 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: 41af549b-dce8-457b-8193-7a70e8d2047c

📥 Commits

Reviewing files that changed from the base of the PR and between ab57c09 and 4d776a9.

📒 Files selected for processing (11)
  • apps/web/client/src/server/api/routers/chat/conversation.ts
  • apps/web/client/src/server/api/routers/chat/message.ts
  • apps/web/client/src/server/api/routers/project/branch.ts
  • apps/web/client/src/server/api/routers/project/createRequest.ts
  • apps/web/client/src/server/api/routers/project/fork.ts
  • apps/web/client/src/server/api/routers/project/frame.ts
  • apps/web/client/src/server/api/routers/project/helper.ts
  • apps/web/client/src/server/api/routers/project/invitation.ts
  • apps/web/client/src/server/api/routers/project/member.ts
  • apps/web/client/src/server/api/routers/project/project.ts
  • apps/web/client/src/server/api/routers/project/settings.ts

.input(branchInsertSchema)
.mutation(async ({ ctx, input }) => {
try {
await verifyProjectAccess(ctx.db, ctx.user.id, input.projectId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Authorization errors swallowed by try-catch blocks in branch.ts and frame.ts. Six procedures place the new verify*Access call inside an existing try-catch that catches all errors and returns false. The authorization Error('Unauthorized or not found') is caught, logged to console, and the client receives false — indistinguishable from a generic DB failure. The security goal (preventing the operation) is met, but the client cannot handle authorization failures.

  • apps/web/client/src/server/api/routers/project/branch.ts#L50-L50: Move verifyProjectAccess before the try-catch in create.
  • apps/web/client/src/server/api/routers/project/branch.ts#L60-L60: Move verifyBranchAccess before the try-catch in update.
  • apps/web/client/src/server/api/routers/project/branch.ts#L81-L81: Move verifyBranchAccess before the try-catch in delete.
  • apps/web/client/src/server/api/routers/project/frame.ts#L42-L42: Move verifyCanvasAccess before the try-catch in create.
  • apps/web/client/src/server/api/routers/project/frame.ts#L54-L54: Move verifyFrameAccess before the try-catch in update.
  • apps/web/client/src/server/api/routers/project/frame.ts#L75-L75: Move verifyFrameAccess before the try-catch in delete.
📍 Affects 2 files
  • apps/web/client/src/server/api/routers/project/branch.ts#L50-L50 (this comment)
  • apps/web/client/src/server/api/routers/project/branch.ts#L60-L60
  • apps/web/client/src/server/api/routers/project/branch.ts#L81-L81
  • apps/web/client/src/server/api/routers/project/frame.ts#L42-L42
  • apps/web/client/src/server/api/routers/project/frame.ts#L54-L54
  • apps/web/client/src/server/api/routers/project/frame.ts#L75-L75
🤖 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/client/src/server/api/routers/project/branch.ts` at line 50, Move
the authorization checks outside the broad try-catch blocks so authorization
errors propagate to clients instead of being converted to false. In
apps/web/client/src/server/api/routers/project/branch.ts lines 50, 60, and 81,
update create to call verifyProjectAccess before its try-catch and update/delete
to call verifyBranchAccess before their try-catches; in
apps/web/client/src/server/api/routers/project/frame.ts lines 42, 54, and 75,
likewise move verifyCanvasAccess or verifyFrameAccess before the corresponding
create, update, and delete try-catches.

Comment on lines +95 to +104
if (messageIds.length === 0) {
return;
}
const rows = await db.query.messages.findMany({
where: inArray(messages.id, messageIds),
with: { conversation: true },
});
if (rows.length !== messageIds.length) {
throw new Error('Unauthorized or not found');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Duplicate messageIds cause false authorization failure.

inArray in SQL returns distinct rows regardless of duplicate values in the input array. If a caller passes ['a', 'a', 'b'], the query returns 2 rows but messageIds.length is 3, so the rows.length !== messageIds.length check throws 'Unauthorized or not found' even though all messages exist and are accessible. Deduplicate before comparing.

🐛 Proposed fix
 export async function verifyMessagesAccess(
     db: DbOrTx,
     userId: string,
     messageIds: string[],
 ): Promise<void> {
-    if (messageIds.length === 0) {
+    const uniqueMessageIds = [...new Set(messageIds)];
+    if (uniqueMessageIds.length === 0) {
         return;
     }
     const rows = await db.query.messages.findMany({
-        where: inArray(messages.id, messageIds),
+        where: inArray(messages.id, uniqueMessageIds),
         with: { conversation: true },
     });
-    if (rows.length !== messageIds.length) {
+    if (rows.length !== uniqueMessageIds.length) {
         throw new Error('Unauthorized or not found');
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (messageIds.length === 0) {
return;
}
const rows = await db.query.messages.findMany({
where: inArray(messages.id, messageIds),
with: { conversation: true },
});
if (rows.length !== messageIds.length) {
throw new Error('Unauthorized or not found');
}
export async function verifyMessagesAccess(
db: DbOrTx,
userId: string,
messageIds: string[],
): Promise<void> {
const uniqueMessageIds = [...new Set(messageIds)];
if (uniqueMessageIds.length === 0) {
return;
}
const rows = await db.query.messages.findMany({
where: inArray(messages.id, uniqueMessageIds),
with: { conversation: true },
});
if (rows.length !== uniqueMessageIds.length) {
throw new Error('Unauthorized or not found');
}
}
🤖 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/client/src/server/api/routers/project/helper.ts` around lines 95 -
104, Deduplicate messageIds before the findMany query and authorization count
check so repeated IDs do not cause a false mismatch. Update the logic around
messageIds and rows.length to compare against the unique ID count while
preserving the existing empty-input return and unauthorized/not-found error
behavior.

Comment on lines +363 to 370
// A user may only list their own preview projects — the userId is
// part of the input schema for backwards-compat client shape, but
// is not trusted; the session's own id is the authorization source.
if (input.userId !== ctx.user.id) {
throw new Error('Unauthorized');
}
const projects = await ctx.db.query.userProjects.findMany({
where: eq(userProjects.userId, input.userId),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

getPreviewProjects throws plain Error instead of TRPCError and still uses input.userId in the query.

Two concerns:

  1. Line 367 throws new Error('Unauthorized') — inconsistent with the rest of the file which uses TRPCError (e.g., captureScreenshot, delete). In production, tRPC converts non-TRPCError throws to INTERNAL_SERVER_ERROR (500) and hides the message, so the client gets a generic 500 instead of a proper 401.

  2. Line 370 uses input.userId in the where clause even though the check on line 366 guarantees it equals ctx.user.id. Using ctx.user.id directly would remain safe even if the guard is later removed or modified.

🔧 Proposed fix
 .query(async ({ ctx, input }) => {
     // A user may only list their own preview projects — the userId is
     // part of the input schema for backwards-compat client shape, but
     // is not trusted; the session's own id is the authorization source.
     if (input.userId !== ctx.user.id) {
-        throw new Error('Unauthorized');
+        throw new TRPCError({
+            code: 'UNAUTHORIZED',
+            message: 'Cannot view preview projects for another user',
+        });
     }
     const projects = await ctx.db.query.userProjects.findMany({
-        where: eq(userProjects.userId, input.userId),
+        where: eq(userProjects.userId, ctx.user.id),
         with: {
             project: true,
         },
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// A user may only list their own preview projects — the userId is
// part of the input schema for backwards-compat client shape, but
// is not trusted; the session's own id is the authorization source.
if (input.userId !== ctx.user.id) {
throw new Error('Unauthorized');
}
const projects = await ctx.db.query.userProjects.findMany({
where: eq(userProjects.userId, input.userId),
// A user may only list their own preview projects — the userId is
// part of the input schema for backwards-compat client shape, but
// is not trusted; the session's own id is the authorization source.
if (input.userId !== ctx.user.id) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Cannot view preview projects for another user',
});
}
const projects = await ctx.db.query.userProjects.findMany({
where: eq(userProjects.userId, ctx.user.id),
🤖 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/client/src/server/api/routers/project/project.ts` around lines 363 -
370, Update getPreviewProjects to throw the file’s established TRPCError type
with the appropriate unauthorized code instead of a plain Error, and use
ctx.user.id rather than input.userId in the userProjects query filter. Preserve
the existing identity validation before querying.

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.

Cross-user IDOR across multiple tRPC procedures

1 participant