Skip to content

MDR API logs DATABASE_URL with plaintext credentials at startup #938

@bjagg

Description

@bjagg

Discovered during the 2026-05-26 dev re-deploy session.

What's happening

components/lif/mdr_utils/database_setup.py (or the equivalent caller) emits an INFO log line at MDR API startup that contains the full DATABASE_URL, including the Postgres password in plaintext:

2026-05-26 19:02:50,272 [INFO] lif.mdr_utils.database_setup:
DATABASE_URL : postgresql+asyncpg://postgres:<redacted-but-real-password>@devmdrdb.dev.aws:5432/devMdrDb

This is written to CloudWatch on every task start, in the shared dev log group (and presumably the equivalent on demo). Anyone with logs:FilterLogEvents on that group can recover the dev DB password by tailing the stream during startup.

Why this matters

  • CloudWatch log retention defaults to "never expire" unless explicitly set, so each startup line is a long-lived plaintext credential
  • The dev log group is shared across services and likely has broader-than-needed read permissions
  • Demo (which is effectively prod for the 2026-05-27 client demo) almost certainly has the same code path

Pre-existing? Yes

This is not new in #884 — it's in MDR boot code that's been there since well before the self-serve feature work. We caught it because the new ECS deploy today caused a fresh startup and the log was visible. Linking under Self-Serve MDR: Security review / threat model for visibility, even though it's not strictly a #884 issue.

Fix sketch

In components/lif/mdr_utils/database_setup.py (or wherever DATABASE_URL is logged), redact the credential before logging:

from urllib.parse import urlparse, urlunparse

def _redact_url(url: str) -> str:
    parts = urlparse(url)
    if parts.password:
        netloc = f"{parts.username}:***@{parts.hostname}"
        if parts.port:
            netloc += f":{parts.port}"
        parts = parts._replace(netloc=netloc)
    return urlunparse(parts)

logger.info("DATABASE_URL : %s", _redact_url(url))

Or omit the URL entirely from the log — the password isn't needed for operator debugging, only the host/db/user are.

Follow-up

  • After the fix lands, rotate the dev + demo DB passwords (the current ones are in CloudWatch history)
  • Audit other services in the repo (graphql_*, translator_*, advisor_api, etc.) for similar log-leak patterns

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    In progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions