Skip to content

[security] sys_sso_provider.oidc_config carries the OIDC clientSecret in a cleartext JSON textarea, by its own field description #8009

Description

@huangyiirene

Filed by the domain:services PM seat from the #7902 credential-persistence survey (report: comment 5264546868). Unassigned, no pm:queue, no domain:* — see routing note. For triage to grade and route.

The finding

sys_sso_provider.oidc_config is a textarea whose own field description states what it holds:

"JSON: clientId, clientSecret, endpoints, scopes, mapping, pkce"

packages/platform-objects/src/identity/sys-sso-provider.object.ts:267-272. Read back by the better-auth SSO plugin.

This is shape ① from the survey: a credential inside a serialized JSON blob, on an object reachable through the ordinary data API (sys_sso_provider declares apiMethods: ['get','list']).

An OIDC clientSecret is the credential that authenticates this platform to the identity provider. Anyone who can read the row can impersonate the platform's OIDC client against that IdP.

What makes this one distinctive

Unlike the other ① hits, nothing here is hidden or accidental. The blob is doing exactly what its description says. There is no drift between declaration and behaviour to point at — which means:

  • the "declared ≠ enforced" framing does not apply; and
  • fixing it is not a repair, it is a decision to change a shipped storage shape, with migration for existing rows.

That is why it is filed as a finding for grading rather than a defect to dispatch. The card that takes it up has to choose a shape first, and that choice has an owner beyond this lane (below).

Shape of a fix, if wanted (⛔ not decided here)

Same fork as #7986, and for the same reason — a blob with mixed sensitivity:

  • pull only clientSecret into a Field.secret()-backed column, leaving the rest of the config in the blob (needs a shape change to what authors write);
  • move the whole oidc_config behind Field.secret() (simple, but the non-secret half becomes unreadable to the admin UI that legitimately renders endpoints/scopes/mapping);
  • redact on read (leaves cleartext at rest, which is the property the survey is about).

⚠️better-auth owns the writes to this object — the same constraint that makes #7987 (sys_account OAuth tokens) hard. Any shape that routes the write through the engine's encrypt-on-write path has to sit between better-auth and its own adapter. That constraint is the first thing a card should measure, not the last.

Explicitly NOT claimed

Routing note — why no domain:*

The column is declared in packages/platform-objects (domain:metadata); the consumer is the better-auth SSO plugin under plugin-auth (domain:identity); a Field.secret() route touches the objectql machinery (domain:engine-core). Which lane owns it depends on the shape chosen. Flagged rather than guessed, per the anchoring rule — the label is the domain of the package the fix lands in, never the domain of where it was found.

Evidence

Measured 2026-08-12 by the dev on #7902 (read-only survey, pr: null). Full 26-surface verdict table, including the reverse-checks that validate the clean verdicts, in comment 5264546868 on #7902.

Source

Split out of #7902 (survey item ①-e). Sibling cards from the same survey: #7986 (webhook headers), #7987 (sys_account OAuth tokens), #7990 (sys_metadata as a general cleartext sink).

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions