fix(server): stop logging the whole environment config when its name is missing - #1087
fix(server): stop logging the whole environment config when its name is missing#1087mlennie wants to merge 1 commit into
Conversation
…is missing
`getProcessedPublisherConfig` skips an environment whose `name` is missing or
not a string, and logged `{ environment }` alongside the warning. An
`Environment` carries `connections` and `storageDestinations`, so that line
put every credential in the environment into the log: Postgres passwords,
BigQuery service-account keys, Snowflake private keys, and the rest.
The values are real by the time the warning fires. `getProcessedPublisherConfig`
reads through `getPublisherConfig`, which returns `processConfigValue(rawConfig)`,
and that deep-walks the config substituting `${VAR}` references, so a
`${MALLOY_X_PASSWORD}` has already become the password. Nothing downstream
redacts it either: `redactSensitive` is called at the request/response
middleware and the axios-error path, not registered in the winston format
chain, so logger metadata reaches the transport verbatim.
Log the entry's index instead. The name is precisely what is missing, so
position is the only safe way to point an operator at the offending entry.
The regression test asserts on the whole logged payload rather than on the
absence of an `environment` key, so re-introducing the config under a
different key still fails it. Three mutations were run against it and each
turns it red: restoring `{ environment }`, deleting the warning, and dropping
the `continue` so the entry is no longer skipped.
This is the same class as the connection-config leak at `connection.ts:1129`
caught on #736. The sibling site in this file, the missing-`name` connection
warning in `convertConnectionsToApiConnections`, is deliberately untouched:
it is already fixed on #1071, and duplicating it here would collide.
Scope, by method rather than by sample. Every `logger.*` and `console.*` call
under `packages/` was parsed as a balanced-paren span, then classified two
ways: a shorthand, spread or `key: ident` inside an object-literal argument,
and a bare identifier passed as a non-first argument. The second classifier
matters, and an earlier pass that lacked it missed 63 sites. At this commit
that is 884 call sites across 606 files, 549 of them spanning more than one
line; 302 put a whole variable into the log; exactly one of those variables
is config-shaped, and it is the `convertConnectionsToApiConnections` site
that #1071 fixes. Before this change there were two.
Of the remaining whole-variable sites, `execute_query_tool.ts` logs the MCP
tool's `params` at info. It carries no credentials, so it is not this class,
but it does log `query` and `givens`, and `givens` can hold row-level-access
identity values. Left for a follow-up issue rather than folded in.
Signed-off-by: Monty Lennie <montylennie@gmail.com>
Sha-Bang
left a comment
There was a problem hiding this comment.
swapping { environment } for { index } is the right fix for that skip, and the test would catch putting the object back. same file still logs { connection: conn } when a connection has no name — that's the same leak. #1071 changes that line too, but it's a big open PR and these hunks don't overlap; landing this without it still ships the dump. fold the connection log into this one? approving, one inline.
| logger.warn( | ||
| `Invalid environment in ${PUBLISHER_CONFIG_NAME}: missing or invalid "name" field. Skipping entry.`, | ||
| { environment }, | ||
| { index }, | ||
| ); |
There was a problem hiding this comment.
this is the one. a bit earlier in the same file, convertConnectionsToApiConnections still passes { connection: conn } into logger.warn on a missing name. same substituted credentials, same moment (bad config, someone pasting logs). #1071 rewrites that metadata to { type }, but it doesn't have to wait on that PR — the hunks don't collide. what if we did the same thing here and pinned it the way this test pins the environment dump?
What this fixes
getProcessedPublisherConfigskips an environment whosenameis missing or is not a string, and logged the whole entry alongside the warning:logger.warn(..., { environment }). AnEnvironmentcarriesconnectionsandstorageDestinations, so that single line put every credential in that environment into the log. Postgres passwords, BigQuery service-account keys, Snowflake private keys, S3/GCS/Azure keys, whatever the environment declares.The values are real by the time the warning fires, which is what makes this a leak rather than untidiness.
getProcessedPublisherConfigreads throughgetPublisherConfig, which returnsprocessConfigValue(rawConfig), and that deep-walks the config substituting${VAR}references. A${MALLOY_X_PASSWORD}has already become the password before any log line sees it.Nothing downstream catches it either.
redactSensitiveis called at the request/response middleware and on the axios-error path, but it is not registered in the winston format chain (createLoggercombines onlyuncolorize,timestamp,errorsandjson), so logger metadata reaches the transport verbatim.It also fires at the worst moment: a malformed config is exactly when an operator is reading logs and pasting them into a ticket.
This is the same class as the connection-config leak at
connection.ts:1129that was caught on #736.The fix
Log the entry's index instead of the entry. The
nameis precisely what is missing, so position is the only safe thing left to point an operator at the offending entry with.Deliberately not included
The sibling site in this same file, the missing-
namewarning inconvertConnectionsToApiConnections, is left untouched. It is already fixed in #1071, and changing it here would collide with that PR.mcp/tools/execute_query_tool.tslogs the MCP tool'sparamsat info level. It carries no credentials so it is not this class, but it does logqueryandgivens, andgivenscan hold row-level-access identity values. Left for a separate issue rather than folded in, following the scope rule set on #736.Scope, by method rather than by sample
Every
logger.*andconsole.*call underpackages/was parsed as a balanced-paren span, because the call at issue spans multiple lines and a line-oriented grep cannot see it. Each call's arguments were then classified two ways: a shorthand, spread orkey: identinside an object-literal argument, and a bare identifier passed as a non-first argument.The second classifier is load-bearing. An earlier pass that lacked it missed 63 sites, and any count taken from it would have been a sample reported as a census.
At this commit that is 884 call sites across 606 files, 549 of them spanning more than one line. 302 put a whole variable into the log. Exactly one of those variables is config-shaped, and it is the
convertConnectionsToApiConnectionssite that #1071 fixes. Before this change there were two. The same script run against the unfixed file reports two, so the before and after are symmetric.The rest of the credential-leak checklist came back clean. No
set -xorbash -xin any of the twelve workflows; secrets are piped togrep -q, redirected into files, or validated withjq ... > /dev/null, and onlyproject_idreachesGITHUB_ENV. No test prints a connection: the credential-adjacentconsole.*calls are all "Skipping: X credentials not configured" strings. GitHub Actions secret masking is not treated as the mitigation, since winston re-escapes quotes and inserts whitespace, so the masker may not recognise the bytes.How it was verified
The regression test asserts on the whole logged payload rather than on the absence of an
environmentkey, so re-introducing the config under a different key still fails it.It was mutation-tested three ways, and each mutation turns it red while restoring returns it green: putting
{ environment }back, deleting the warning entirely, and dropping thecontinueso the entry is no longer skipped. So it pins the leak, the warning still firing, and the entry still being skipped. On the first of those the failure output is itself the report, showing the substituted plaintext password inside the logged payload.Gate on this branch: typecheck clean, lint clean, prettier clean on both files touched. Unit 3217 pass / 3 skip / 0 fail across 152 files. Integration 387 pass / 0 fail across 40 files, and that number needs a caveat: three of those forty files are the bundled storefront example's own tests, collected out of gitignored
publisher_databecausetest:integrationpassestestsas a substring path filter rather than a directory. The server's own files are 37, and a run against a cleanpublisher_datareports 315 across 37.