Found while implementing #8696 (the mysql/mongodb DSN arms), and filed rather than fixed there — it is a different arm reached by a different mechanism, one layer below where #8696 sits.
The claim this falsifies
#8696 and its triage both treat the postgres arm as the reference implementation that already honours a bound secret on its DSN branch:
// default-datasource-driver-factory.ts, buildSqlConnectionif(url){return{connectionString: url,
...(spec.secret ? {password: spec.secret} : {}),
...
};}That is true at the knex-config layer and false at the client layer. pg merges the parsed connection string over the explicit config, so the injected password is discarded before the handshake.
Measured
pg 8.22.0, lib/connection-parameters.js:
// if the config has a connectionString defined, parse IT into the config we use// this will override other default values with what is stored in connectionStringif(config.connectionString){config=Object.assign({},config,parse(config.connectionString))}Driving that constructor directly:
new ConnectionParameters({connectionString:'postgresql://app@db.internal:5432/app', password:'INJECTED'})
-> effective password = null # the bound secret is gone
new ConnectionParameters({connectionString:'postgresql://app:embedded@db.internal:5432/app', password:'INJECTED'})
-> effective password = 'embedded' # the DSN wins even against a bound secret
Object.assign copies the parsed password key even when it is null, so a credential-free DSN — the only shape #8082 permits — actively erases the injected value rather than leaving it in place.
Why it matters
Same consequence as #8696, same fail-quietly shape: DatasourceConnectionService resolves external.credentialsRef fail-closed, the operator sees a bound credential and a datasource that reports connected, and the connection was opened with no password. Post-#8082 an authored url is credential-free, so this is the shape the connection form produces. Not a disclosure — a broken binding.
It is also worse than #8696 in one respect: this arm looks fixed. It has an explicit spec.secret branch and a comment stating the intent ("For a DSN, a separately-supplied secret overrides the embedded password"), and any test that asserts the factory's emitted connection object passes. The defect is only visible one layer down, which is why #8696's pin asserts the shape it does and carries a note not to copy it here.
Note for whoever takes it: the arms merge in OPPOSITE directions
Do not fix this by symmetry with the mysql arm landed in #8696. Measured:
| client | shape | precedence |
|---|
mysql2 3.23.1 | { uri, password } | explicit key wins (uri only fills what the caller omitted) |
pg 8.22.0 | { connectionString, password } | connection string wins (Object.assign over the config) |
So pg needs a different remedy from the one that works for mysql — candidates: parse the DSN into discrete fields and re-attach the secret; or keep connectionString and re-serialise the userinfo. Both are behaviour-affecting for stored rows and want the measurement done before the shape is chosen.
Scope note
Sibling of #8696, not a duplicate: that card names the mysql and mongodb arms, whose builders drop the secret before it ever reaches a client. Fixing them does not touch this. Filed unassigned for triage.
Found while implementing #8696 (the mysql/mongodb DSN arms), and filed rather than fixed there — it is a different arm reached by a different mechanism, one layer below where #8696 sits.
The claim this falsifies
#8696 and its triage both treat the postgres arm as the reference implementation that already honours a bound secret on its DSN branch:
That is true at the knex-config layer and false at the client layer.
pgmerges the parsed connection string over the explicit config, so the injected password is discarded before the handshake.Measured
pg8.22.0,lib/connection-parameters.js:Driving that constructor directly:
Object.assigncopies the parsedpasswordkey even when it isnull, so a credential-free DSN — the only shape #8082 permits — actively erases the injected value rather than leaving it in place.Why it matters
Same consequence as #8696, same fail-quietly shape:
DatasourceConnectionServiceresolvesexternal.credentialsReffail-closed, the operator sees a bound credential and a datasource that reports connected, and the connection was opened with no password. Post-#8082 an authoredurlis credential-free, so this is the shape the connection form produces. Not a disclosure — a broken binding.It is also worse than #8696 in one respect: this arm looks fixed. It has an explicit
spec.secretbranch and a comment stating the intent ("For a DSN, a separately-supplied secret overrides the embedded password"), and any test that asserts the factory's emittedconnectionobject passes. The defect is only visible one layer down, which is why #8696's pin asserts the shape it does and carries a note not to copy it here.Note for whoever takes it: the arms merge in OPPOSITE directions
Do not fix this by symmetry with the mysql arm landed in #8696. Measured:
mysql23.23.1{ uri, password }pg8.22.0{ connectionString, password }Object.assignover the config)So
pgneeds a different remedy from the one that works for mysql — candidates: parse the DSN into discrete fields and re-attach the secret; or keepconnectionStringand re-serialise the userinfo. Both are behaviour-affecting for stored rows and want the measurement done before the shape is chosen.Scope note
Sibling of #8696, not a duplicate: that card names the mysql and mongodb arms, whose builders drop the secret before it ever reaches a client. Fixing them does not touch this. Filed unassigned for triage.