Skip to content

finding: survey — which other config objects still keep credentials in a JSON blob column, now that resolveSecretField() makes the Field.secret() channel readable #7902

Description

@huangyiirene

Observation-class finding, filed unassigned with no pm:queue — for triage to grade and route. Offered by the dev at the end of #7799 / PR #7901 ("the issue's 'worth checking in the same pass' line is a real second surface, but it is a survey, not this defect — happy to file it if you want it tracked"); the services PM took the offer rather than let it evaporate with the session.

Why this is worth a card now, and was not before

Until PR #7901, the Field.secret() channel was effectively write-only. Measured on that PR:

  • maskSecretFields (packages/objectql/src/engine.ts:4744) replaces the stored ref with SECRET_MASK on everyfind/findOne, unconditionally and after hooks, with no isSystem carve-out.
  • So resolveSecret's own docstring — "privileged consumers use resolveSecret() against the stored ref" — described a path no consumer could enter, because there was no supported way to obtain the ref.
  • The observable consequence: subsystems that needed a credential back out of storage did not use the field type. packages/services/service-datasource/src/datasource-secret-binder.ts keeps its handle in a plain unmasked column; plugin-webhooks kept the HMAC secret inside sys_webhook.definition_json as cleartext ([security] The webhook signing secret is stored in cleartext in sys_webhook.definition_json #7799).

PR #7901 adds ObjectQL.resolveSecretField(object, recordId, field) — a type-guarded read that refuses any field not declared type: 'secret'. That removes the reason those workarounds existed. So the question "who else is storing a credential somewhere a normal read can reach?" changes from "nothing much can be done about it" to "each one now has an available plugin-local fix."

What the survey should cover

For every system/config object that persists third-party or outbound credentials, determine whether the credential is:

  1. inside a serialized JSON blob column (the [security] The webhook signing secret is stored in cleartext in sys_webhook.definition_json #7799 shape — recoverable through the ordinary data API by anyone who can read the row), or
  2. in a plain unmasked column (the datasource-binder shape — narrower, but still not encrypted at rest), or
  3. behind Field.secret() (encrypted into sys_secret, masked on read).

Named starting points, from #7799's measurement — not a claim that these are all of them, and not a claim that any of them is currently broken:

  • packages/services/service-datasource — the binder's plain ref column (deliberate, pre-resolveSecretField; worth re-deciding now).
  • Connector credential storage under packages/connectors/* and the connector registry.
  • Anything else persisting an outbound auth material: plugin-email transport credentials, service-sms provider keys, service-storage backend credentials, embedder-openai / knowledge-ragflow API keys.

The deliverable of the survey is a list with a verdict per object, not a fix — each hit that turns out to be shape ① or ② is its own card, sized on its own migration story (existing rows, fail-closed behaviour with no CryptoProvider, and whether the consumer can tolerate a decrypt at read time).

Explicitly NOT claimed

Routing note

The survey spans several lanes (domain:services owns most of the plugin/connector landing sites; domain:engine-core owns the objectql channel; domain:cli/repo:cloud may own host-side credential wiring), so this is a natural split-into-per-lane-findings card once graded — per the services seat's standing discipline ⑫. Triage's call on whether to grade it as one survey card or fan it out immediately.

Evidence

Measured 2026-08-12 by the dev on PR #7901, reading packages/objectql/src/secret-fields.ts, packages/objectql/src/engine.ts:4640-4838, packages/services/service-datasource/src/datasource-secret-binder.ts, and packages/cli/src/commands/serve.ts:2691-2712. Full measurement in the <!-- os-dev-report --> comment on #7799.

Source

Offered by the dev on #7799 (PR #7901) as an explicitly out-of-scope second surface.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions