Skip to content

fluid validate prints a URL-userinfo password in cleartext: the console sanitiser only matches keyword=value #754

Open

Description

@fas89

fluid validate prints a password from the document it was handed, in cleartext, while masking a GitHub token in the same string. The console's sanitiser is a keyword-=-value matcher, so a credential that appears as URL userinfo — scheme://user:password@host, the shape every JDBC/DSN connection string uses — has no keyword in front of it and passes through untouched.

This is pre-existing, not a regression: it reproduces identically on main.

Reproduction

A contract whose root id is a DSN (any field the schema rejects will do — the error quotes the value):

fluidVersion: "0.7.5"
kind: DataProduct
id: "snowflake://svc_etl:S3cr3t-Pa55w0rd@acme-prod.snowflakecomputing.com/?token=ghp_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
name: orders
metadata:
  owner: {team: commerce}
exposes:
  - exposeId: orders
    kind: table
    binding: {platform: local, format: parquet, location: {path: ./orders}}
    contract: {schema: [{name: id, type: string}]}
$ fluid validate c.fluid.yaml --offline
 1. id: 'snowflake://svc_etl:S3cr3t-Pa55w0rd@acme-prod.snowflakecomputing.com/?token=<redacted> does not match '^[A-Za-z0-9_]...'

The password is in the clear. The token in the same string is <redacted> — which is the useful part of the signal: a sanitiser is on this path and is doing something, so this reads as protected when it is only half protected.

Why

fluid_build/_console.py:59:

_SECRET_OUTPUT_PATTERN = re.compile(
    r"(?i)\b(password|passphrase|token|secret|api[_ -]?key)\b(\s*[:=]\s*)([^\s,;]+)"
)

It requires a literal keyword before the value. ?token=ghp_… matches; svc_etl:S3cr3t-Pa55w0rd@ does not, because nothing says "password" in front of it.

_redact_str, which applies that pattern, carries this docstring:

Every code path that emits potentially sensitive text to a user-facing sink must terminate on a call to this function.

So it is declared as the sanitiser, and cli/validate.py has comments asserting "redaction stays on the path" and "The redaction pass in cprint still runs". Both are true and neither is sufficient.

The repo's other redactor already handles this shape:

>>> from fluid_build.observability.secret_redactor import redact_secret_text
>>> redact_secret_text("id: 'snowflake://svc_etl:S3cr3t-Pa55w0rd@host/?token=ghp_AAAA'")
"id: 'snowflake://svc_etl:***REDACTED***@host/?token=***REDACTED***'"

So the two layers are asymmetric in the direction that matters, and the weaker one is the one terminating the user-facing path. CLAUDE.md already warns that the redaction layers must be extended together to stay symmetric; it names observability/secret_redactor.py and the Snowflake-provider-local one, and does not mention _console.py::_redact_str, which is a third.

Impact

Low severity, and worth being precise about rather than inflating. The value comes from a document the operator passed in, so this is not an attacker extracting the operator's own secret. The realistic harm is a credential belonging to someone else — in a third-party, vendor-supplied or generated contract — being echoed into a terminal and, more durably, into CI logs, which are retained and world-readable on a public repository.

Suggested fix

Add a URL-userinfo alternative to _SECRET_OUTPUT_PATTERN, so scheme://user:secret@host is masked with the keyword form.

Delegating _redact_str to redact_secret_text would be the better structural answer — one implementation instead of two — but note the constraint before taking it: _console.py is on the fluid --help cold path, which tests/perf/test_startup_budget.py guards with a module ceiling and a forbidden-import set. Any delegation has to keep that budget. _redact_str is also deliberately shaped as a single-typed, closed sanitiser node so CodeQL's taint tracker recognises it, so whatever replaces the body should preserve that property.

Wherever it lands, a test should assert both shapes are masked in one string, since the existing coverage passes today with the password visible.

Already fixed on one surface

fluid odps import's validate-before-write gate had the same leak and was routed through redact_secret_text in #753 (ab8076de), with tests. That fix was deliberately scoped to the surface that PR introduced; this issue is the pre-existing fluid validate path, and the keyword-only pattern behind both.

Area: Other · Priority: Important

Activity

  1. added
    bugSomething isn't working
    securitySecurity-related changes or reports
    on Oct 27, 2026
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

    bugSomething isn't workingsecuritySecurity-related changes or reports

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions