Skip to content

Metadata type: 'api' actions send the Bearer token and X-Tenant-ID to absolute third-party URLs — the #2725 sameOriginOnly mitigation was never applied to this lane #5702

Description

@os-sales

Found while documenting the X-Tenant-ID edge contract for #5279 (PR forthcoming on that card). Not fixed there — different package, different fence.

What was measured

createAuthenticatedFetch({ sameOriginOnly: true }) exists because of #2725: a fetch whose target URL comes from view metadata may name a third-party host that must never see the platform bearer token. Reading every construction site in the repo, exactly one passes the option:

  • packages/app-shell/src/console/ConsoleShell.tsx:213const apiProviderFetch = withSettleSignal(createAuthenticatedFetch({ sameOriginOnly: true }));, the provider: 'api' data-source lane.

Every other site builds the bare wrapper, including the one that takes a URL from metadata just as freely:

  • packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:300const authFetch = useMemo(() => createAuthenticatedFetch(), []);

That wrapper is what apiHandler calls, and apiHandler accepts an absolute target straight from the action metadata (useConsoleActionRuntime.tsx:315-337):

const isAbsolute = targetStr.startsWith('/') || /^https?:\/\//i.test(targetStr);
...
const url = resolvedTarget.startsWith('http') ? resolvedTarget : `${baseUrl}${resolvedTarget}`;

So a metadata type: 'api' action whose target is https://third-party.example/... is fetched through the bare wrapper. Two headers ride along:

HeaderGate in createAuthenticatedFetchSent to a third-party host?
Authorization: Bearer <session token>isApiCall = /\/api\//i.test(url)Yes, whenever the third-party URL happens to contain /api/ — which is the common shape for one
X-Tenant-ID: <active organization id>none at allYes, unconditionally

The bearer half is already pinned as intended-for-the-other-lane by an existing test — packages/auth/src/__tests__/createAuthenticatedFetch.test.tsx, "without sameOriginOnly, cross-origin /api/ URLs keep the legacy attach behaviour". That test records the wrapper's behaviour; it says nothing about whether this call site should be using the bare wrapper.

Why it looks like an oversight rather than a decision

#2725's fix hardened the lane it was reported on. Both lanes take their URL from author-supplied metadata, and nothing in the code or comments distinguishes them on that axis — apiHandler's own comment describes the wrapper as "Bearer + X-Tenant-ID + same-origin cookies" without noting that the target may be off-origin.

Not covered by this report

Whether the tenant stamp should be gated on isApiCall at all (it is the only one of the three headers that is not) is a separate question raised on #5279. It is listed here only because it changes what a third-party host receives from this call site.

Suggested repair, for triage rather than as a decision

useConsoleActionRuntime.tsx:300 builds createAuthenticatedFetch({ sameOriginOnly: true }), matching ConsoleShell. That would stop attaching credentials to off-origin action targets while leaving every same-origin action untouched. It is a behaviour change for any deployment that today relies on the bearer reaching an off-origin /api/ target, so it needs a deliberate call, not a drive-by.

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions