Skip to content

[security] GET /api/v1/meta/:type/:name/diff serves stored credential VALUES — a plane the per-type item redactor cannot reach #8671

Description

@qq9340100

Found while implementing #8154 (the metadata read path's per-type redaction hook). Filed rather than fixed: #8154's scope is settled by ruling to four item-serving exits plus the saveMetaItem carry-forward, and this exit needs a different decision, not the same call one line over. Not assigned — recording, not starting.

What leaks

ObjectStackProtocolImplementation.diffMetaItem (packages/metadata-protocol/src/protocol.ts) reads two stored bodies — history rows straight out of sys_metadata_history, and the current active row via repo.get(), which is documented VERBATIM (no conversion, no redaction) — runs diffShallow over them, and returns:

{ added: [{ path, value }], removed: [{ path, value }], changed: [{ path, from, to }] }

Those are stored body values. It is routed and live: packages/rest/src/rest-server.ts mounts GET ${metaPath}/:type/:name/diff and answers res.json(result) unmodified.

So for a pre-#8078datasource row whose credential changed between two versions, the response carries something of the form { path: 'config.password', from: '…', to: '…' } in cleartext — and for a row where the credential was added, an added entry with the same value.

Why #8154's hook does not reach it

#8154 consumes getMetadataTypeRedactor (#8300) and applies it to whole item bodies at the four serving exits. A diff is not an item body: it is a list of (path, value) triples, so there is no body for the item-level redactor to take. The same reason #7990's data-API plane is a separate card applies here — same stored bytes, different plane.

Confirmed not a duplicate: searched open issues by keyword (diffMetaItem, diff endpoint, redaction, history, credential) and by file path before filing. #7990 owns the ordinary data API over sys_metadata; #8154 owns the four item exits; neither names this one.

Why it is not a one-liner — the decision this needs

Redacting both bodies before diffShallow is the obvious fix and it is not obviously right: two redacted bodies differ in nothing at a redacted path, so a credential rotation becomes an invisible no-diff. The version history is exactly where an operator would look to answer "when did this credential change, and by whom" — and #8154's own _diagnostics ordering ruling exists because a redaction that silently deleted an operator's inventory was the worse outcome. Plausible options, none free:

  • A — redact both bodies before diffing. Simple, reuses the seam. Cost: rotations vanish from history entirely.
  • B — diff raw, then redact the emitted value / from / to at any path the type's redactor names, keeping the path and a "changed" signal. Keeps the audit fact, drops the secret. Cost: needs a path-level projection the registry does not expose today (redactedKeys is item-relative and per-body, so this is a real derivation, not a call).
  • C — refuse the diff for types that carry a redactor unless the caller holds a stronger permission. Cost: takes a working Studio affordance away from the rows most likely to need inspecting.

B looks right to me on #8154's own reasoning (preserve the operator's ability to enumerate, remove only the secret), but I did not measure it and it is not my call to take here.

Also measured while in the area, so nobody re-measures it

Both were owed by #8154 and are recorded there too:

  • SysMetadataRepository.getByHash — an ADR-0009 MetadataRepository contract method implemented by four repositories, with no production caller anywhere in the repo: every reference outside the implementations is the contract suite, unit tests, or a docblock. It cannot serve a body to anyone today. Worth knowing when a consumer is eventually wired — the redaction belongs at that consumer's exit, not in the repository, whose raw get() the connect path and the write path both depend on.
  • SysMetadataRepository.restoreVersion — a WRITE. Both callers (rollbackMetaItem, revertCommit) return receipts (success / version / seq / restoredFromVersion / message) and no body; the restored body reaches only applyRegistryWriteThrough, which must stay raw. No leak, and redacting there would corrupt the restored row.

Related: #8154, #7990, #8078, #8081.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions