You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
While working on #1982 I audited every entry point in apps/web that accepts a caller-supplied resource ID, since that advisory looked like an instance of a pattern rather than a one-off. It was — I found four, and opened a PR for each.
The pattern
Cap has a solid access-control primitive in VideosPolicy.canView, and most code uses it. The failures all have the same shape: a second entry point to the same data that skips the check. Server actions are the main way this happens, because a "use server" function is independently callable by any client and does not inherit whatever gating its usual API route does.
read transcription status of anyone's private video
Two are worth calling out specifically:
#2030 is the sharpest./api/analytics/route.tsdoes gate on canView, with a comment stating it exists so private view counts aren't disclosed. But getVideoAnalytics is a server action with zero auth of any kind — no getCurrentUser, no policy — and a "use client" component (Analytics.tsx) already calls it directly. The route's protection is bypassable by calling the action the same way the app's own client code does.
#2031 leaks IDs, which are the key to everything else. Video IDs are what /s/{videoId} and the other video endpoints take, so enumerating a private space's ID set is useful to an attacker even before any per-video check runs.
Sweep result
After these four, I re-swept: no remaining videoId-taking server action or API route in apps/web lacks an access check. Two files still show up in a naive grep but are fine on inspection — /api/notifications takes no ID and only returns the caller's own rows, and /api/tools/loom-download takes a Loom video ID, not a Cap resource.
Notes on the fixes
Every fix reuses an existing primitive (VideosPolicy.canView, or getSpaceAccess for the space paths) rather than introducing new logic, so they inherit the full existing model — owner, org, space, public, password, email-domain restrictions.
While working on #1982 I audited every entry point in
apps/webthat accepts a caller-supplied resource ID, since that advisory looked like an instance of a pattern rather than a one-off. It was — I found four, and opened a PR for each.The pattern
Cap has a solid access-control primitive in
VideosPolicy.canView, and most code uses it. The failures all have the same shape: a second entry point to the same data that skips the check. Server actions are the main way this happens, because a"use server"function is independently callable by any client and does not inherit whatever gating its usual API route does.What I found
newCommentactiongetVideoAnalyticsactiongetSpaceVideoIds/getFolderVideoIds/api/video/transcribe/statusTwo are worth calling out specifically:
#2030 is the sharpest.
/api/analytics/route.tsdoes gate oncanView, with a comment stating it exists so private view counts aren't disclosed. ButgetVideoAnalyticsis a server action with zero auth of any kind — nogetCurrentUser, no policy — and a"use client"component (Analytics.tsx) already calls it directly. The route's protection is bypassable by calling the action the same way the app's own client code does.#2031 leaks IDs, which are the key to everything else. Video IDs are what
/s/{videoId}and the other video endpoints take, so enumerating a private space's ID set is useful to an attacker even before any per-video check runs.Sweep result
After these four, I re-swept: no remaining
videoId-taking server action or API route inapps/weblacks an access check. Two files still show up in a naive grep but are fine on inspection —/api/notificationstakes no ID and only returns the caller's own rows, and/api/tools/loom-downloadtakes a Loom video ID, not a Cap resource.Notes on the fixes
VideosPolicy.canView, orgetSpaceAccessfor the space paths) rather than introducing new logic, so they inherit the full existing model — owner, org, space, public, password, email-domain restrictions.getSpaceAccessand notrequireSpaceManager: these are read paths, and requiring manager would have locked out ordinary members.Happy to consolidate them into a single PR if you'd prefer to review it that way.
cc @richiemcilroy