fix(web): enforce video access policy before creating comments (#1982)#2029
fix(web): enforce video access policy before creating comments (#1982)#2029DPS0340 wants to merge 3 commits into
Conversation
The newComment server action authenticated the caller but never checked whether they may access the target video. Since videoId comes from the client, any signed-in user could insert comments into another user's private recording. Gate the insert behind VideosPolicy.canView, matching the pattern already used by getTranscript and the /api/video/ai fix in CapSoftware#1926.
| ).pipe(Policy.withPublicPolicy(videosPolicy.canView(videoId))); | ||
| }).pipe(provideOptionalAuth, EffectRuntime.runPromiseExit); | ||
|
|
||
| if (Exit.isFailure(accessExit) || accessExit.value.length === 0) { |
There was a problem hiding this comment.
Minor: splitting the Exit.isFailure check from the .value access lets the type guard narrow cleanly (and matches the pattern used in get-transcript.ts).
| if (Exit.isFailure(accessExit) || accessExit.value.length === 0) { | |
| if (Exit.isFailure(accessExit)) { | |
| throw new Error("Video not found"); | |
| } | |
| if (accessExit.value.length === 0) { | |
| throw new Error("Video not found"); | |
| } |
|
@tembo please review |
Addresses review feedback: separating Exit.isFailure from the .value access lets the type guard narrow cleanly and matches get-transcript.ts. Also limits the lookup to one row since videos.id is unique.
|
Adopted — pushed. Split the branches so the type guard narrows cleanly, matching |
|
@tembo please re-review |
| import { comments } from "@cap/database/schema"; | ||
| import { comments, videos } from "@cap/database/schema"; | ||
| import { provideOptionalAuth, VideosPolicy } from "@cap/web-backend"; | ||
| import type { ImageUpload } from "@cap/web-domain"; |
There was a problem hiding this comment.
Minor perf/consistency nit: provideOptionalAuth will call getServerSession() + query users again. Since this action already requires auth and you already have user, you can provide the current-user layer directly and skip the extra DB hit.
| import type { ImageUpload } from "@cap/web-domain"; | |
| import { makeCurrentUserLayer, VideosPolicy } from "@cap/web-backend"; |
| .limit(1), | ||
| ).pipe(Policy.withPublicPolicy(videosPolicy.canView(videoId))); | ||
| }).pipe(provideOptionalAuth, EffectRuntime.runPromiseExit); | ||
|
|
There was a problem hiding this comment.
Following on the above import, this avoids re-fetching the current user inside the Effect env.
| }).pipe(Effect.provide(makeCurrentUserLayer(user)), EffectRuntime.runPromiseExit); |
Addresses review feedback: newComment already requires auth and holds the user, so provideOptionalAuth would redundantly call getServerSession() and re-query users. Provide the current-user layer directly.
|
Adopted — pushed. Verified the redundancy before changing it: const user = yield* getCurrentUser; // getServerSession() + select from users
return yield* user.pipe(Option.match({
onNone: () => app,
onSome: (user) => app.pipe(Effect.provide(makeCurrentUserLayer(user))),
}));so on a path that has already established Now Small bonus: this also removed a |
Same redundancy tembo flagged on CapSoftware#2029: the route already requires auth and holds the user, so provideOptionalAuth would re-run getServerSession() and re-query users to build the same layer.
|
@tembo please re-review |
1 similar comment
|
@tembo please re-review |
Fixes #1982 (GHSA-5r3r-v2f5-wqwh).
Problem
The
newCommentserver action authenticates the caller, then inserts the comment without ever checking whether that user is allowed to access the target video:videoIdis a client-supplied argument to a server action, so being signed in is the only barrier. Any authenticated user can post comments — or emoji reactions, or threaded replies — into any video by ID, including another user's private recording. The comment then shows up in the owner's activity feed and triggers a notification to them viacreateNotification.Worth noting the same file already treats deletion correctly:
deleteCommentverifiesauthorIdbefore removing anything. Creation was the gap.Fix
Gate the insert behind the existing
VideosPolicy.canViewpolicy before any write happens.I deliberately did not invent a new access-check shape — this mirrors the pattern already used in this exact directory by
getTranscript(apps/web/actions/videos/get-transcript.ts), and it is the same policy the/api/video/aifix in #1926 used to close the sibling advisory #1981:Because it reuses
canView, it inherits the full existing access model for free — owner, org membership, space membership, public videos, password-protected videos, and email-domain restrictions — rather than approximating it with an ownership check that would have broken legitimate commenting on shared videos.The error message is deliberately
"Video not found"rather than"Access denied", so this does not become an oracle for probing which video IDs exist.Verification
biome check apps/web/actions/videos/new-comment.ts— clean.tsc --noEmitonapps/web: the remaining diagnostics on this file (TS6305, plus TS2488/TS18046 on the Effect generator) are not introduced by this change — the untouchedget-transcript.tsemits the identical set, since it uses the same pattern and the workspacetsconfigproject references aren't built in a plaintscrun. Verified by diffing the two files' diagnostics.@cap/web-domain,@cap/web-backendand@cap/databasebuild clean via turbo.Diff: 1 file, +22/-2.