Skip to content

postgres.Migrate writes the database password to the pod log on a malformed DATABASE_URL #178

Description

@mrautela365

A malformed DATABASE_URL causes the full DSN, including the password, to be written to the pod log at startup and again on every background retry. Verified end-to-end on origin/main, not in a PR branch.

Mechanism

(*url.Error).Error() renders as fmt.Sprintf("%s %q: %s", Op, URL, Err) — it embeds the URL it was given.

The subtle part: no format string in our code names the URL. migrate.NewWithSourceInstance → database.Open parses the DSN internally and returns a *url.Error already carrying it. internal/infrastructure/postgres/pool.go:138 then wraps it:

return fmt.Errorf("init migrator: %w", err)

and internal/container/container.go:948 calls postgres.Migrate(dsn), surfacing the result through slog.Warn at startup and on each retry.

So a grep for url.Parse does not find this site — the parse happens inside a dependency.

Why unwrapping is not sufficient

The obvious remedy — unwrap to drop the outer URL — still leaks. net/url causes quote the fragment they choked on, so:

postgres://user:<password>@host/db     ->  invalid URL escape "%zz"

where the quoted fragment is part of the password itself. This was found by mutation testing on #159: a fix that unwrapped instead of discarding survived the mutation.

The form that holds is to discard the cause entirely for *url.Error and substitute a sentinel, which is what internal/platform/llm already does under its own no-echo rule. Note safeErrSummary normalises and truncates — it is not a redactor and must not be used as one here.

Four net/url failure modes were confirmed to leak: invalid percent-escape, invalid port, unclosed IPv6 bracket, and a control character in the URL.

Scope

#159 fixed 9 test-path sites (dbtest.go, invalid_index_live_test.go, the migrate-down harness). This production path was deliberately left out: turning an approved, test-only PR into a production change was not a trade worth making mid-review.

Suggested fix

Route the Migrate error through the same discard-the-cause treatment, and add a test that renders the error for a password-bearing DSN and asserts the password is absent. Mutation-verify it: reverting the fix must make the test fail, and an unwrap-only variant must also fail — otherwise the test is not pinning the real behaviour.

Keep diagnosability: an operator must still be able to distinguish a malformed DSN from a connection failure, so replace the cause with a sentinel rather than dropping the error.

cc @dealako

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions