Skip to content

fix(security): enforce referenced-object RLS/FLS on $expand (#2850) - #2961

Merged
os-zhuang merged 2 commits into
mainfrom
claude/expand-rls-fls-bypass-ogj4wf
Jul 15, 2026
Merged

fix(security): enforce referenced-object RLS/FLS on $expand (#2850)#2961
os-zhuang merged 2 commits into
mainfrom
claude/expand-rls-fls-bypass-ogj4wf

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #2850.

Problem

ObjectQL.expandRelatedRecords (packages/objectql/src/engine.ts) resolved lookup / master_detail / user references by calling the driver directly (driver.find), never re-entering the security middleware for the referenced object. Only tenantId was threaded, so the referenced object's RLS, FLS, and CRUD gate were skipped — cross-tenant was the only surviving boundary. The middleware's post-pass FLS masking covers only the top-level object; field-masker does not recurse into expanded records.

Impact: any API-key/session caller who could read a base row could ?expand=<fk> and receive the full referenced record — including rows the referenced object's owner-only RLS would hide and fields FLS would mask. Reachable via GET /data/:object/:id?expand=… and POST /data/:object/query (both funnel through engine.find/findOne).

Fix

Route the expand batch through the engine's own this.find for the referenced object instead of the raw driver, so the security middleware applies that object's RLS + FLS to the collected id $in [...] batch. This re-enters the middleware once per level (no N+1) and preserves the existing batched load and depth guard (nested expand still recurses manually, so MAX_EXPAND_DEPTH is unaffected).

The sub-read is tagged with a server-set __expandRead context marker (executionContext is server-built, never client-supplied — consistent with the existing internal __readScope/__writeScope markers). The security middleware uses it to implement the surgical policy agreed on the issue:

  • PUBLIC referenced object → waive only the object-level CRUD / requiredPermissions gate (the row is already broadly readable via the '*' wildcard grant, so applying that gate to an expansion would over-block common status/owner lookups without adding protection). RLS injection + FLS masking still run.
  • PRIVATE referenced object → keep the full gate (RLS + FLS + CRUD). Expansion may reveal only rows the caller could have read directly; a private object the caller can't read gracefully retains the bare FK id.

No per-field escape hatch is introduced (option A on the issue).

Changes

  • packages/objectql/src/engine.tsexpandRelatedRecords resolves via this.find(referenceObject, …, { context: { __expandRead: true } }).
  • packages/plugins/plugin-security/src/security-plugin.ts — compute expandSkipCrud (find + __expandRead + public), and AND it into the requiredPermissions (step 1.5) and CRUD (step 2) gates only.

Tests

  • engine.test.ts — new: the expand sub-read routes through the middleware for the referenced object tagged __expandRead; three existing expand assertions updated to reflect the secured path (the referenced driver.find now carries the query options/context instead of undefined).
  • security-plugin.test.ts — new: __expandRead waives the CRUD gate for a public referenced object, does not waive it for a private one, and still injects RLS on the expand sub-read.

All @objectstack/objectql (853) and @objectstack/plugin-security (172) tests pass; both packages typecheck; the real-$expand rest/export-integration test passes.


Generated by Claude Code

`expandRelatedRecords` resolved lookup/master_detail/user references by
calling the driver directly, so the REFERENCED object's row- and
field-level security never ran — only tenant isolation survived. Any
API/session caller who could read a base row could `?expand=` a foreign
key and receive the full referenced record, including RLS-hidden rows and
FLS-masked fields.

Route the expand batch through the engine's own `find` so the security
middleware applies the referenced object's RLS + FLS to the `id $in [...]`
batch (one query, no N+1). The read is tagged with a server-set
`__expandRead` context marker; the security middleware waives ONLY the
object-level CRUD / requiredPermissions gate for PUBLIC referenced objects
(already broadly readable — avoids over-blocking common status/owner
lookups), while PRIVATE referenced objects keep the full gate. RLS
injection and FLS masking run for both. Covers the list and single-record
REST/protocol surfaces (find + findOne).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vh6cpe9MV7bMiXvooeWLH9
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Canceled Canceled Jul 15, 2026 1:28pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/plugin-security.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-security)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/plugin-security)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/plugin-security)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vh6cpe9MV7bMiXvooeWLH9
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 15, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 15, 2026 13:36
@os-zhuang
os-zhuang merged commit 8b27dd7 into main Jul 15, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/expand-rls-fls-bypass-ogj4wf branch July 15, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: $expand bypasses RLS/FLS on the referenced object (data leak on lookups)

2 participants