Make the stdout guard survive DOTENV_CONFIG_QUIET - #21
Merged
Merged
Conversation
Follow-up to the previous commit, from an independent review of it: `quiet: true`
alone is defeatable, so the guard it provides was weaker than documented.
dotenv resolves quiet as
processEnv.DOTENV_CONFIG_QUIET || options.quiet (lib/main.js:248)
and RE-READS it from processEnv after populate (lib/main.js:292). The env var
therefore outranks the code option, parseBoolean('false') is false, and the
second read means a DOTENV_CONFIG_QUIET=false inside the .env file being loaded
defeats it too — not just one exported in the shell. Either way the injection
banner returns to stdout, which for this server is the JSON-RPC transport, and
the failure mode is a client registering zero tools with no error to read.
This makes it unreachable rather than merely discouraged: console.log is routed
to stderr for the duration of the config call and restored in `finally`, so the
banner cannot reach stdout however quiet resolves — and anything else that logs
during env loading is caught with it. `quiet: true` stays as the primary, so in
the normal case nothing is emitted at all.
tests/integration/helpers.ts deliberately does NOT get the swap: it is test-only,
its stdout is not a transport, and the db-guard pinning regex matches the call
expression, which is unchanged — so both files still resolve DATABASE_URL
identically.
Two guards added. One pins the swap in db.ts. The other is behavioural and proves
the swap is load-bearing rather than redundant: it sets DOTENV_CONFIG_QUIET=false,
calls dotenv with quiet:true, and asserts a banner IS produced — if that ever goes
empty, dotenv changed its precedence and the swap should be re-evaluated before
removal.
Verified: 32/32 in the two guard suites, tsc clean on TypeScript 7.0.2, and a
controlled comparison with DOTENV_CONFIG_QUIET=false in the environment — unfixed
writes 1 stdout line and it is the banner, fixed writes 0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #20, which is already merged. An independent review of that change found the guard it added was weaker than its own documentation claimed.
The gap
quiet: truealone is defeatable. dotenv resolves it asThe environment variable outranks the code option, and
parseBoolean('false')isfalse. Because line 292 re-reads it after the.envfile is loaded, aDOTENV_CONFIG_QUIET=falseinside the.envbeing read defeats it too — not only one exported in the shell.Either way the injection banner returns to stdout, which for this server is the JSON-RPC transport, and the failure mode is a client registering zero tools with no error to read.
#20 documented this precedence and treated it as "don't do that". This makes it unreachable instead.
The fix
console.logis routed to stderr for the duration of the config call and restored infinally, so the banner cannot reach stdout howeverquietresolves — and anything else that logs during env loading is caught with it.quiet: truestays as the primary, so in the normal case nothing is emitted at all.tests/integration/helpers.tsdeliberately does not get the swap: it is test-only, its stdout is not a transport, and the db-guard pinning regex matches the call expression, which is unchanged — so both files still resolveDATABASE_URLidentically.Two guards
One pins the swap in
db.ts. The other is behavioural, and proves the swap is load-bearing rather than redundant: it setsDOTENV_CONFIG_QUIET=false, calls dotenv withquiet: true, and asserts a banner is produced. If that ever goes empty, dotenv changed its precedence and the swap should be re-evaluated before removal — rather than silently becoming a no-op nobody notices.That guard passed the moment it was written, which is how we know the vulnerability was real and not theoretical.
Verification
tscclean on TypeScript 7.0.2DOTENV_CONFIG_QUIET=falsein the environment: unfixed writes 1 stdout line and it is the banner; fixed writes 0The 7
embedfailures on a local run are pre-existing and environmental —bge-smallisn't cached in a fresh clone, and CI pre-caches it in a dedicated step.