Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions apps/web/actions/folders/get-folder-videos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { sharedVideos, spaceVideos } from "@cap/database/schema";
import type { Folder, Space, Video } from "@cap/web-domain";
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { getSpaceAccess } from "@/actions/organization/space-authorization";

export async function getFolderVideoIds(
folderId: Folder.FolderId,
Expand All @@ -21,17 +22,47 @@ export async function getFolderVideoIds(
throw new Error("Folder ID is required");
}

if (!spaceId) {
throw new Error("Space ID is required");
}

const isAllSpacesEntry = user.activeOrganizationId === spaceId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor: consider validating spaceId up front (like getSpaceVideoIds), so a missing/empty value doesn't fall through to getSpaceAccess and return a misleading "Folder not found".

Suggested change
const isAllSpacesEntry = user.activeOrganizationId === spaceId;
if (!spaceId) {
throw new Error("Space ID is required");
}


// `spaceId` is caller-supplied, so the space branch needs a membership
// check. The all-spaces branch compares against `activeOrganizationId`,
// which is read from the user record, so it is already scoped.
//
// The membership check alone is not sufficient: `folderId` is also
// caller-supplied, so the queries below must be constrained to the space
// (or org) we just authorized. Otherwise a caller could pass a space they
// legitimately belong to together with a folder from another space and
// still read its contents.
if (!isAllSpacesEntry) {
const access = await getSpaceAccess(user.id, spaceId);
if (!access || (!access.organizationRole && !access.spaceRole)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good hardening. One concern: this new access check keys off the caller-supplied spaceId, but the actual query below only filters by folderId (no spaceId / org constraint). If someone can obtain/guess a folderId from another space/org, they could pass a space they do have access to and still enumerate that folder’s video IDs. Might be worth also scoping the query by spaceVideos.spaceId in the space branch, and by sharedVideos.organizationId (ideally user.activeOrganizationId, not spaceId) in the all-spaces branch.

throw new Error("Folder not found");
}
}

const rows = isAllSpacesEntry
? await db()
.select({ id: sharedVideos.videoId })
.from(sharedVideos)
.where(eq(sharedVideos.folderId, folderId))
.where(
and(
eq(sharedVideos.folderId, folderId),
eq(sharedVideos.organizationId, user.activeOrganizationId),
),
)
: await db()
.select({ id: spaceVideos.videoId })
.from(spaceVideos)
.where(eq(spaceVideos.folderId, folderId));
.where(
and(
eq(spaceVideos.folderId, folderId),
eq(spaceVideos.spaceId, spaceId),
),
);

return {
success: true,
Expand Down
11 changes: 11 additions & 0 deletions apps/web/actions/spaces/get-space-videos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getCurrentUser } from "@cap/database/auth/session";
import { sharedVideos, spaceVideos } from "@cap/database/schema";
import type { Space } from "@cap/web-domain";
import { and, eq, isNull } from "drizzle-orm";
import { getSpaceAccess } from "@/actions/organization/space-authorization";

export async function getSpaceVideoIds(spaceId: Space.SpaceIdOrOrganisationId) {
try {
Expand All @@ -20,6 +21,16 @@ export async function getSpaceVideoIds(spaceId: Space.SpaceIdOrOrganisationId) {

const isAllSpacesEntry = user.activeOrganizationId === spaceId;

// `spaceId` comes from the caller, so being signed in is not enough.
// Without this, any authenticated user can enumerate the video IDs of a
// space in an organization they do not belong to.
if (!isAllSpacesEntry) {
const access = await getSpaceAccess(user.id, spaceId);
if (!access || (!access.organizationRole && !access.spaceRole)) {
throw new Error("Space not found");
}
}

const videoIds = isAllSpacesEntry
? await db()
.select({
Expand Down