Skip to content

mongo config.options.auth.password is a fourth spelling of an inline credential — authorable, persisted cleartext, unredacted, and read by the client #9040

Description

@qq9340100

Found while implementing #8696's mongodb arm (the DSN-branch secret injection), and filed rather than fixed there: it is a producer/authoring-door defect in packages/spec, independent of that card's connect-time fix. Deliberately left unlabeled for triage to grade.

What happens

MongoConfigSchema.options is the MongoClient passthrough, declared as placeholderFreeDeep(z.record(z.string(), z.unknown())). Nothing in it refuses credential material, so a mongo datasource can author its password as options.auth.password — and every existing refusal steps around it:

options.auth is the same secret one syntax over from all three, exactly as #8337 was one syntax over from #8082.

Measured on origin/main @ 792524c22

Authoring — accepted clean, no issues:

MongoConfigSchema.safeParse({url: 'mongodb://app@h:27017/app',options: {auth: {username: 'app',password: 'PLAINTEXT-IN-METADATA'}},})// => success: true, options preserved verbatim

Read path — not redacted, so the cleartext comes back out of the admin API as well as sitting in sys_metadata:

redactDatasourceConfig('mongodb',{url: 'mongodb://app@h:27017/app',options: {auth: {username: 'app',password: 'PLAINTEXT-IN-METADATA'}}})// => { config: { …auth.password: 'PLAINTEXT-IN-METADATA' }, redactedKeys: [] }

redactableConfigKeys('mongodb') is ['password','passwd','pwd','token','jwt','auth_token','authtoken'] — a top-level key-name list, and the value here is two levels down.

And it genuinely reaches the client: MongoDBDriver spreads config.options into MongoClientOptions, so auth resolves into MongoClient's credentials. That is measured in passing by #8696's new pin (bound-secret-dsn-branches.test.ts), which asserts a bound external.credentialsRefoutranks a passthrough auth block — before that change the passthrough's password was what the client used.

Why it matters

This is the at-rest half, not a connect-time bug: the value is written into sys_metadata in cleartext by an authoring path the platform accepts without comment, and handed back unredacted to anything reading the datasource record. #8082's ruling chose to refuse rather than accept residual risk for precisely this shape, and #8337 extended that to the query-string spelling on the same grounds.

What a fix has to decide

  1. Refuse at publish (the [Decision] URL-embedded credentials (user:password@host in driver config.url) remain a live cleartext door after #7990 — refuse at publish, or accept as residual risk? #8082/turso: ?authToken= in an authored config.url / config.syncUrl query string is credential material the #8082 userinfo refusal does not cover #8337 direction): a superRefine on options rejecting credential-bearing paths — at minimum auth.password, with a message pointing at external.credentialsRef / the connection form's secret field, the same remedy the other three refusals name. Needs a decision on how deep the refusal walks and whether it is mongo-only or a shared rule for every driver's passthrough block (postgres/mysql options-equivalents should be checked — this issue measured only mongo).
  2. Redact at rest/read — strictly weaker on its own: it hides the value from the API while leaving cleartext in the store, which is the residual risk [Decision] URL-embedded credentials (user:password@host in driver config.url) remain a live cleartext door after #7990 — refuse at publish, or accept as residual risk? #8082 declined.

⚠️ Note for whoever takes it: a refusal must not break the legitimate contents of this passthrough (replicaSet, tls, timeouts). And auth.username is not credential material — #8876's accessor documents the same asymmetry for userinfo, where only a non-empty password is treated as a secret.

Where the fix lands

packages/spec/src/data/driver/mongo.zod.ts (+ common.zod.ts if the rule is shared), and the redaction table if route 2 is taken as well. Backlink: #8696.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions