fix(web): add ownership checks to folder, space and org video actions#1928
fix(web): add ownership checks to folder, space and org video actions#1928richiemcilroy wants to merge 3 commits into
Conversation
|
Paragon Review Skipped Hi @richiemcilroy! Your Polarity credit balance is insufficient to complete this review. Please visit https://app.paragon.run to finish your review. |
|
@greptileai please review the PR |
|
@greptileai please review the PR |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
@greptileai please review the PR |
| import type { Folder, Space, Video } from "@cap/web-domain"; | ||
| import { and, eq } from "drizzle-orm"; | ||
| import { revalidatePath } from "next/cache"; | ||
| import { requireOrganizationSettingsManager } from "@/actions/organization/authorization"; |
There was a problem hiding this comment.
If the goal is to allow regular org members to move their own shared videos from the “All Spaces” view, this import can switch to getOrganizationAccess (and drop the settings-manager requirement below).
| import { requireOrganizationSettingsManager } from "@/actions/organization/authorization"; | |
| import { getOrganizationAccess } from "@/actions/organization/authorization"; |
| @@ -75,12 +106,21 @@ export async function moveVideoToFolder({ | |||
| ), | |||
| ); | |||
There was a problem hiding this comment.
requireOrganizationSettingsManager makes this path admin/owner-only, but the underlying UPDATE currently isn’t scoped to the caller’s own videos. That combination blocks normal members and still lets admins move anyone’s shared videos.
Consider using org membership + scoping the UPDATE to sharedByUserId so members can only move videos they shared.
| } else if (spaceId && isAllSpacesEntry) { | |
| const access = await getOrganizationAccess(user.id, user.activeOrganizationId); | |
| if (!access) { | |
| throw new Error("Organization not found"); | |
| } | |
| // The destination must be an org-level (non-space) folder. | |
| if (destinationFolder && destinationFolder.spaceId !== null) { | |
| throw new Error("Folder not found or not accessible"); | |
| } | |
| await db() | |
| .update(sharedVideos) | |
| .set({ | |
| folderId: folderId === null ? null : folderId, | |
| }) | |
| .where( | |
| and( | |
| eq(sharedVideos.videoId, videoId), | |
| eq(sharedVideos.organizationId, user.activeOrganizationId), | |
| eq(sharedVideos.sharedByUserId, user.id), | |
| ), | |
| ); |
| const [folder] = await db() | ||
| .select({ id: folders.id, spaceId: folders.spaceId }) | ||
| .select({ | ||
| id: folders.id, | ||
| spaceId: folders.spaceId, | ||
| createdById: folders.createdById, | ||
| }) | ||
| .from(folders) | ||
| .where(eq(folders.id, folderId)); |
There was a problem hiding this comment.
The folder lookup is not scoped to
user.activeOrganizationId. In moveVideoToFolder.ts the equivalent query correctly adds eq(folders.organizationId, user.activeOrganizationId) to the WHERE clause. Without it, a caller who created a personal folder in a different organisation (or holds canManage over a space in another org) can pass that foreign folderId, pass the ownership check, and the subsequent inserts will write sharedVideos/spaceVideos rows for the current org that carry a folderId pointing to another org's folder — corrupting cross-org FK references.
| const [folder] = await db() | |
| .select({ id: folders.id, spaceId: folders.spaceId }) | |
| .select({ | |
| id: folders.id, | |
| spaceId: folders.spaceId, | |
| createdById: folders.createdById, | |
| }) | |
| .from(folders) | |
| .where(eq(folders.id, folderId)); | |
| const [folder] = await db() | |
| .select({ | |
| id: folders.id, | |
| spaceId: folders.spaceId, | |
| createdById: folders.createdById, | |
| }) | |
| .from(folders) | |
| .where( | |
| and( | |
| eq(folders.id, folderId), | |
| eq(folders.organizationId, user.activeOrganizationId), | |
| ), | |
| ); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/actions/folders/add-videos.ts
Line: 33-40
Comment:
The folder lookup is not scoped to `user.activeOrganizationId`. In `moveVideoToFolder.ts` the equivalent query correctly adds `eq(folders.organizationId, user.activeOrganizationId)` to the WHERE clause. Without it, a caller who created a personal folder in a different organisation (or holds `canManage` over a space in another org) can pass that foreign `folderId`, pass the ownership check, and the subsequent inserts will write `sharedVideos`/`spaceVideos` rows for the current org that carry a `folderId` pointing to another org's folder — corrupting cross-org FK references.
```suggestion
const [folder] = await db()
.select({
id: folders.id,
spaceId: folders.spaceId,
createdById: folders.createdById,
})
.from(folders)
.where(
and(
eq(folders.id, folderId),
eq(folders.organizationId, user.activeOrganizationId),
),
);
```
How can I resolve this? If you propose a fix, please make it concise.
Adds ownership and membership checks to the folder, space and organization video actions so a user can only move, add to, or list videos they actually have access to.
Greptile Summary
This PR adds ownership and membership gates to six server actions and one page that previously disclosed or mutated video/folder data without verifying whether the caller actually had access. The approach — creator-only for personal folders, role-check via
getSpaceAccess/getOrganizationAccessfor space/org-backed folders — is consistent across all changed files and correctly mirrors the policy inFoldersPolicy.moveVideoToFolder.tscorrectly scopes the destination-folder lookup touser.activeOrganizationIdand addseq(videos.ownerId, user.id)to the personal-move UPDATE.get-organization-videos.tsandget-space-videos.tsadd the expected membership guards and correctly distinguish the all-spaces (org-ID) case from a real space ID.add-videos.tsandget-folder-videos.tsintroduce new folder lookups but omiteq(folders.organizationId, user.activeOrganizationId)from the WHERE clause, unlike the equivalent lookup inmoveVideoToFolder.ts— leaving a cross-org boundary gap.Confidence Score: 4/5
Mostly safe, but two of the six changed files have a cross-org boundary gap in their new folder lookups that should be patched before merging.
The folder lookups added in
add-videos.tsandget-folder-videos.tsdo not constrain the query touser.activeOrganizationId, unlike the equivalent lookup inmoveVideoToFolder.tsin the same PR. A user who created or has manage access to a folder in another organisation can pass that foreign folder ID, pass the ownership/role check, and either probe cross-org folder existence or insertsharedVideos/spaceVideosrows in the current org that reference a foreign folder's primary key.apps/web/actions/folders/add-videos.ts and apps/web/actions/folders/get-folder-videos.ts — both need
eq(folders.organizationId, user.activeOrganizationId)added to their new folder WHERE clauses.Security Review
add-videos.ts,get-folder-videos.ts): Both new folder lookups are missingeq(folders.organizationId, user.activeOrganizationId). A user who created (or hascanManageaccess to) a folder in a different organisation can pass that foreignfolderId, pass the ownership/role check, and either probe folder existence across org boundaries (get-folder-videos.ts) or writesharedVideos/spaceVideosrows for the current org that carry a cross-orgfolderIdFK (add-videos.ts).moveVideoToFolder.tsin the same PR correctly scopes its lookup with the org constraint.Important Files Changed
Prompt To Fix All With AI
Reviews (4): Last reviewed commit: "fix(web): validate destination folder ma..." | Re-trigger Greptile