Skip to content

GET /api/v1/marketplace/install-local serves the installed-package ledger to anonymous callers — no authentication at all #9011

Description

@qq9340100

Found while implementing #8976 (the capability gate on the four mutating install-local routes). Out of scope there by ruling — that card's 裁决 names the four mutating doors and says the fix is authorization on those — so filing separately rather than riding it in. Unassigned.

Measured (origin/main, packages/cloud-connection/src/marketplace-install-local-plugin.ts)

handleList — the handler mounted at GET /api/v1/marketplace/install-local — calls no identity resolution whatsoever. Its first statement reads the ledger:

privatehandleList=async(c: any,ctx: PluginContext): Promise<Response>=>{const{ entries, skipped }=this.readAll();

The four sibling routes (POST /install-local, DELETE …/install-local/:id, …/reseed-sample-data, …/purge-sample-data) each opened with requireAuthenticatedUser before #8976 and now open with a manage_metadata capability gate. The GET opens with nothing — it is not a weaker gate, it is the absence of one, so an anonymous caller who can reach the port gets a 200.

Re-check by symbol (line numbers in this repo go stale within hours):

git grep -n "handleList" -- packages/cloud-connection/src/marketplace-install-local-plugin.ts
git grep -n "rawApp.get(ROUTE_BASE" -- packages/cloud-connection/src/marketplace-install-local-plugin.ts

What the response body discloses

Per ledger entry: packageId, versionId, manifestId, version, installedAt, installedBy, withSampleData — plus, once per response, storageDir.

  • installedBy is a platform user id, and the listing enumerates them across every install.
  • storageDir is an absolute filesystem path on the host (#6721 added it deliberately, for a signed-in CLI operator who cannot see the remote host's disk).
  • The package inventory itself is a version-level software bill of materials for the deployment — which packages, at which versions, installed when.

Why it is worth a decision rather than an obvious patch

The mount is deliberately unconditional (cloud#1287, so air-gapped boxes stop 404ing), and the console's "Installed Apps" page is a real consumer of this route, so the question is which posture the read should take, not whether to remove it. At least three defensible answers:

  1. Authenticated floor — any signed-in principal may list. Cheapest, matches "the console shows it in Setup".
  2. The same manage_metadata the write doors now demand — consistent with the operator-grade framing of the surface, but would hide the Installed Apps list from non-operator users the console currently shows it to.
  3. Authenticated floor + field narrowing — list the packages, drop installedBy and storageDir for callers without an authoring capability. Splits "what is installed here" from "who installed it and where it lives on disk", which are genuinely different sensitivities.

The storageDir half interacts with #6721's stated purpose, so whichever way it goes should be decided knowingly rather than as a side effect.

Severity

Read-only disclosure, so lower than #8976's write channel — but it needs no credential of any kind, and on the walled multi-org EE shape (cloud#1331's surveyed deployment) the inventory and the installer identities are cross-tenant information the Layer 0 wall does not scope either, for the same reason: metadata is environment-scoped, not org-scoped.

Related: #8976 (the mutating routes; now gated), #6603 / #8919 (the /meta write-door precedent), cloud#1331 (walled-deployment context), #6721 (why storageDir is in this payload).

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions