Repository navigation
fluid validate prints a URL-userinfo password in cleartext: the console sanitiser only matches keyword=value #754
Description
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