Skip to content

The stdio MCP record resource reads rows through its own getRecord seam, skipping the ADR-0049 apiEnabled / apiMethods exposure gate #8266

Description

@os-zhuang

Found while implementing #8083 (applying the exposure gate on the stdio MCP data bridge). Filed rather than fixed there: #8083's scope is the bridge, and this is a different seam in a different file.

What differs

#8083 closed the declaration leak on the six object-CRUD verbs, which all flow through createStdioDataBridge (packages/mcp/src/stdio-data-bridge.ts). The ADR-0101 record resource does not use that bridge at all. Its reader is a separate closure built inline in the plugin:

  • packages/mcp/src/plugin.tsgetRecord = async (objectName, recordId) = ... calls scopedQl.find(objectName, { where: { id: recordId }, limit: 1, context: ec }) directly
  • handed to this.runtime.bridgeResources(metadataService, getRecord)
  • served as objectstack://objects/{objectName}/records/{recordId}

So after #8083 an object declaring enable.apiEnabled: false is refused by get_record (the tool) and still readable through the record resource, on the same transport, under the same key.

Why it matters

Same grading as #8083, and for the same documented reason: this is a surface-area declaration leak, not an authorization bypass.packages/runtime/src/api-exposure.ts records the gate as a surface-area control rather than the authorization boundary, and this read passes the engine's CRUD / FLS / RLS middleware exactly as before — it goes through ql.find with the key's ExecutionContext. What leaks is the author's exposure declaration. Please do not triage this as a security issue.

Scope note: the gap predates #8034 — the record resource has read rows over ql.find without the gate since #7645, which is the narrower form of the divergence #8083's body records under "Scope note".

Suggested shape (not prescriptive)

#8083 landed the gate as enforceApiExposure inside stdio-data-bridge.ts, taking its decision from the spec's single source of truth (resolveEffectiveApiMethods / isApiOperationAllowed) rather than re-implementing it. Two routes:

Either way the acceptance shape is the one #8083 used: one declaration, both read paths (get_record tool and the record resource), same verdict.

Overlap to check before dispatch

#7279 touches this exact code. It is about a third hand-written ExecutionContext assembly in the stdio MCP plugin dropping tabPermissions / accessToken, and getRecord's resolvePrincipal is in that blast radius. These two should not be in flight at the same time, and there may be a case for doing them as one card.


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions