Found by the ADR-0090 D10 adversarial security review. Pre-existing, general (affects all callers, not agent-specific).
What
expandRelatedRecords (packages/objectql/src/engine.ts:~1890-1896) resolves the referenced object by calling the driver directly, not this.find():
const expandOpts = this.buildDriverOptions(execCtx);
const relatedRecords = await driver.find(referenceObject, relatedQuery, expandOpts) ?? [];
It never re-enters executeWithMiddleware, so the referenced object's RLS, FLS, and CRUD gate are NOT applied — only tenantId is threaded (buildDriverOptions), so cross-tenant is still blocked. The middleware's post-pass FLS masking (security-plugin.ts ~1023) masks only the top-level object; field-masker does not recurse into expanded/nested records.
Impact
A caller who can legitimately read a base row can ?expand=<fk> and receive the full referenced record — including rows the referenced object's owner-only RLS would hide and fields FLS would mask — for any FK on a row it can see. Tenant isolation is the only surviving boundary.
Reachable via the REST data API: GET /data/:object/:id?expand=… and POST /data/:object/query forward expand (http-dispatcher.ts:~1953). (The MCP query/get tools do not forward expand, so the pure-agent surface is not exposed — but any API-key/session user is.)
Fix direction
Route $expand resolution through the security middleware for the referenced object (RLS + FLS applied per referenced object), or apply the referenced object's read filter + field mask to the expanded batch before attaching. Preserve the batched $in performance (don't N+1).
Refs
packages/objectql/src/engine.ts:~1821-1903 (expandRelatedRecords)
packages/runtime/src/http-dispatcher.ts:~1953-1958 (REST expand forwarding)
Found by the ADR-0090 D10 adversarial security review. Pre-existing, general (affects all callers, not agent-specific).
What
expandRelatedRecords(packages/objectql/src/engine.ts:~1890-1896) resolves the referenced object by calling the driver directly, notthis.find():It never re-enters
executeWithMiddleware, so the referenced object's RLS, FLS, and CRUD gate are NOT applied — onlytenantIdis threaded (buildDriverOptions), so cross-tenant is still blocked. The middleware's post-pass FLS masking (security-plugin.ts~1023) masks only the top-level object;field-maskerdoes not recurse into expanded/nested records.Impact
A caller who can legitimately read a base row can
?expand=<fk>and receive the full referenced record — including rows the referenced object's owner-only RLS would hide and fields FLS would mask — for any FK on a row it can see. Tenant isolation is the only surviving boundary.Reachable via the REST data API:
GET /data/:object/:id?expand=…andPOST /data/:object/queryforwardexpand(http-dispatcher.ts:~1953). (The MCPquery/gettools do not forwardexpand, so the pure-agent surface is not exposed — but any API-key/session user is.)Fix direction
Route
$expandresolution through the security middleware for the referenced object (RLS + FLS applied per referenced object), or apply the referenced object's read filter + field mask to the expanded batch before attaching. Preserve the batched$inperformance (don't N+1).Refs
packages/objectql/src/engine.ts:~1821-1903(expandRelatedRecords)packages/runtime/src/http-dispatcher.ts:~1953-1958(REST expand forwarding)