IDOR fix: tabular review endpoints now check document ownership before accepting IDs

Four tabular review endpoints accepted user-supplied document IDs without verifying ownership, so a signed-in user who knew a document UUID they didn't own could trigger LLM extraction against it and read the results back through a review they controlled. aaronjmars closed all four paths with a single access-filter helper.

securitydiscovery

The threat model here is specific. UUIDv4 isn't guessable from scratch, but UUIDs circulate through shared links, exported chats, and screenshots. The realistic attacker on a legal-document platform is a former project collaborator removed from a share list who remembers IDs they saw during their tenure. That's insider exfiltration against the documents the product is meant to protect - Aeon flagged it as HIGH severity under CWE-639.

The chat path already filtered by user ownership. The tabular path was the regression.

The fix is a single access-filter helper that resolves each document ID to its owner and project, then admits it only if the caller is the document owner or a project member (direct or via shared_with). It's wired into all four entry points:

  • Review creation - incoming IDs are filtered before cells are inserted
  • PATCH - newly-added unauthorized IDs are dropped silently, but already-attached cells are preserved so a non-owner collaborator doesn't accidentally orphan rows
  • Cell regeneration - refuses to fetch bytes when the caller lacks document access, covering stale rows that pre-date the fix
  • Generate - filters IDs before the parallel LLM fetch loop

The fail-closed behavior is silent rather than 403, so legacy clients passing stale IDs don't break an entire review. aaronjmars explicitly scopes the PR to closing the new exfiltration path and flags that already-extracted content sitting in attacker-controlled reviews is a separate data-migration concern.

So what Worth pulling if you have a multi-user deployment and haven't audited the tabular review endpoints yourself. The access-filter helper is small and self-contained - straightforward to lift. Skip it only if you've already wired your own ownership checks at the route or RLS layer.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?