Skip to content

fix(data-connections): stop listing other accounts' connections on product create - #480

Draft
alukach wants to merge 3 commits into
mainfrom
fix/data-connection-owner-listing
Draft

fix(data-connections): stop listing other accounts' connections on product create#480
alukach wants to merge 3 commits into
mainfrom
fix/data-connection-owner-listing

Conversation

@alukach

@alukach alukach commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Found while investigating why a BYOB connection wasn't appearing on /products/new. Not the cause of that — a separate leak on the same path, which #462 did not touch.

The problem

listUsableDataConnections filters only on UseDataConnection and GetDataConnection. Neither looks at ownergetDataConnection returns true for any non-disabled principal, and useDataConnection checks read_only and required_flag and nothing else.

So /products/new serialized every account-owned connection in the table into the page for any logged-in user: bucket names, regions, base prefixes and prefix templates. authentication is stripped, but the rest is not.

ProductCreationForm does filter on !dc.owner || dc.owner === forAccountId (ProductCreationForm.tsx:90-92) — in the browser. It only ever hid them from view.

Demonstrated before fixing: a plain regular-user session received rival--secret with bucket: "rival-private-bucket-name".

The fix

listUsableDataConnections(session, ownerAccountIds) applies the owner rule server-side. The page passes potentialOwnerAccounts, which it already computes for the owner dropdown, so the set is exactly the accounts the user may create products under.

The filter goes in the helper rather than at the call site so a second caller can't reintroduce the leak — the required parameter makes it unskippable.

No behaviour change for legitimate use: system-level (unowned) connections are still offered to everyone, and an account's own connections are still offered to its members.

Scope: this does not close the exposure

GET /api/v1/data-connections returns every connection to any caller, including anonymous ones — getDataConnection is principal?.account?.disabled ? false : true, and for a null principal that expression is falsy, so it returns true. sanitizeDataConnection strips only secret-bearing auth; details (bucket, region, base prefix) and federated role ARNs are returned deliberately, per that helper's doc comment.

So account-owned bucket names remain publicly listable through the API regardless of this PR. This change is still worth making — a page shouldn't ship data its own UI then hides — but it should not be read as closing the hole.

Whether the v1 listing should scope account-owned connections to their owners is a policy question worth deciding separately. The ARN exposure looks intentional (the customer's IAM trust policy is the security boundary); the account-owned bucket names look like a consequence of that decision rather than an intended part of it.

Tests

npx jest: 482 passing. The 5 failures (DropdownSection.integration, 4 analytics card tests) reproduce identically on clean origin/main — verified by running those suites detached at origin/main. tsc reports no new errors; the pre-existing AdminBreakdownChart implicit-any errors are also on main.

Two new tests in data-connections.test.ts, over the real isAuthorized:

  • another account's connection is withheld from the listing entirely
  • system-level connections are still offered to any account

🤖 Generated with Claude Code

…oduct create

listUsableDataConnections filtered only on UseDataConnection and
GetDataConnection, neither of which looks at `owner`. Every account-owned
connection in the table therefore reached /products/new and was serialized
into the page — bucket names, regions and prefix templates included — for any
logged-in user. ProductCreationForm's `!dc.owner || dc.owner === forAccountId`
filter runs in the browser, so it only ever hid them from view.

Take the accounts the user may create products under and apply the same rule
server-side, in the helper rather than at the call site so a future caller
can't reintroduce the leak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
source-cooperative Ready Ready Preview Aug 4, 2026 6:23pm

Request Review

@claude

claude Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 2m 39s —— View job


Review of PR #480

  • Gather context (diff, changed files)
  • Review src/lib/data-connections.ts changes
  • Review src/app/(app)/products/new/page.tsx changes
  • Review test files
  • Post final review

✅ No blocking issues — safe to merge.

  • The core fix is sound: listUsableDataConnections (src/lib/data-connections.ts:81-94) now requires an ownerAccountIds param and filters (!dataConnection.owner || usableBy.has(dataConnection.owner)) before the existing UseDataConnection/GetDataConnection checks. I verified against src/lib/api/authz.ts that neither getDataConnection (:414-423, returns true for any non-disabled principal) nor useDataConnection (:459-479) inspects owner, confirming the leak the PR describes and that this is the correct place to close it.
  • potentialOwnerAccounts in src/app/(app)/products/new/page.tsx:55-68 is filtered to account-wide (!membership.repository_id) Owner/Maintainer memberships, which exactly matches the createRepository authorization rule in authz.ts:1108-1119/hasRole (:1550+) — so the connection list is scoped to precisely the accounts a user could actually create a product under, no wider and no narrower.
  • Only one call site for listUsableDataConnections exists (page.tsx), so the signature change can't leave a second caller unscoped.
  • owner survives DataConnectionObjectSchema.omit({ authentication: true }) (page.tsx:80), so the existing client-side filter in ProductCreationForm.tsx:95-97 still works as defense-in-depth on top of the new server-side filter.
  • New tests in data-connections.test.ts and page.test.tsx cover the negative case (another account's connection withheld) and the legitimate cases (system-level connections still offered, account-wide owner/maintainer included, repo-scoped/read-data/invited memberships excluded) — good coverage of the exact boundary this PR draws.
  • The PR body is transparent that this doesn't close the broader GET /api/v1/data-connections exposure — that's correctly scoped out as a separate policy question, not something this diff should be trying to fix.

No correctness, security, or best-practice issues found in the diff itself.

potentialOwnerAccounts took every membership in state Member, regardless of
role or scope. A read-data member, or someone whose membership covers a single
product, was offered that account as a product owner — an option createProduct
rejects — and, because this list now also scopes which owned data connections
are serialized into the page, it handed them that account's bucket names and
prefixes. That is the enumeration #462 set out to prevent.

Restrict it to account-wide owners and maintainers, which is what
createRepository actually requires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant