Skip to content

fix(config): redact credentials inside odbc_params, and widen the guard's domain (#1206) - #305

Open
wshallwshall wants to merge 1 commit into
mainfrom
claude/fix-odbc-params-redaction
Open

fix(config): redact credentials inside odbc_params, and widen the guard's domain (#1206)#305
wshallwshall wants to merge 1 commit into
mainfrom
claude/fix-odbc-params-redaction

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

redacted_settings masked flat scalars and descended into headers alone, so a credential in odbc_params was returned verbatim by GET /connections/{name}/metadata behind MONITORING_READ and printed by graph --json — on the same object whose top-level password masked correctly.

Measured, both serializers

BEFORE:  odbc_params={"PWD": S, "sslpassword": S}   -> both returned verbatim
         password="p" on the same object            -> '***'

AFTER :  PWD, sslpassword, Password                 -> '***'
         Encrypt, ApplicationIntent,
         TrustServerCertificate, sslkey (a PATH)    -> still readable

Not merely operator misuse

The docstring says odbc_params "carries only static driver keywords", and the typed fields carry exactly one credential. But _reject_envref_odbc_params refuses env() there. So a connection needing a second driver credential — libpq sslpassword beside PWD — has no typed home and no env() form, and the inline literal is the only expressible shape.

A refusal that removes the safe expression while leaving the unsafe one is not a mitigation.

Display fix, not storage fix

Stated because the difference is easy to lose: the credential remains an inline literal in the config file. Keeping it out needs env() to work here, which needs nested settings env-resolved — that changes the resolution path and what the refusal means, so it is the route-onward and deliberately not folded in.

My first attempt fixed nothing

I reached for _is_secret_setting. It returns False for every one of PWD, Password and sslpassword — it matches a fixed frozenset of MessageFoundry settings names, while these are ODBC driver keywords with different spellings and different case.

That version would have masked nothing while reading as a fix — inside the change closing a defect whose whole shape is a control whose domain is narrower than its surface. _is_secret_odbc_key is shape-based and case-insensitive; pwd is listed explicitly because it is an abbreviation matching no substring rule.

The guard I wrote against this class was green over it

test_connection_factory_redaction_domain.py filtered its AST-derived domain through _decorator_style, keeping 4 of 23 spec-returning functions and dropping every base constructor including Database. Its docstring asserted "no shipped factory emits a nested container beyond those declared below" and called the hole "THEORETICAL rather than live". Both false.

That claim is deleted, not softened — a number a test has not established has no business in the file defining the test, and a hedged version keeps the authority while losing the falsifiability.

The domain is now all 23, and test_the_domain_covers_every_spec_returning_function fails if any discovered function is missing from it. Every other control in that file answers is this instrument working — make it fail on purpose, confirm the injection landed, run a negative control, assert it examined something. None answered is it pointed at the whole thing. That is now a test.

Found on the way, and worth more than the fix

Http and Soap refuse an inline intake credential outright and demand env(), so the value never resolves into settings and no serializer can leak it. That is the stronger control odbc_params lacks, and it is now asserted by test_a_refusing_connector_actually_refuses_an_inline_credential rather than left as folklore.

Verification

  • Mutation-proved: removing the descent reds Database and DatabasePoll.
  • 103 tests green across the redaction, connection-API and rotation suites.
  • Three test false positives resolved at the right layer, each with a stated reason: signing_key is a path (direct.py _read_files it, and the engine classifies the passphrase and not the key deliberately); odbc_password_key/odbc_user_key are keyword names, not credentials.

Provenance

Found by the parallel asvs-tracking-rework session's independent assessment of ASVS 15.3.1 — which I had recused from, because I authored the two fixes bearing on that cell. Reproduced here by execution before any code changed.

Fourth instance of this class, and the second time a guard written after the previous instance picked a domain narrower than the surface.

…rd's domain (#1206)

redacted_settings masked flat scalars and descended into `headers` alone, so a
credential in `odbc_params` was returned VERBATIM by /metadata behind
MONITORING_READ and printed by graph --json -- on the SAME object whose top-level
`password` masked correctly.

NOT MERELY OPERATOR MISUSE, which is why it masks rather than warns. The typed
fields carry exactly ONE credential, and _reject_envref_odbc_params refuses env()
in the bag. So a second driver credential (libpq sslpassword beside PWD) has no
typed home and no env() form, and the inline literal is the only expressible
shape. A refusal that removes the SAFE expression while leaving the UNSAFE one is
not a mitigation.

DISPLAY FIX, NOT STORAGE FIX. The credential is still an inline literal in the
config file. Keeping it out needs env() to work here, which needs nested settings
env-resolved -- route-onward, deliberately not folded in.

MY FIRST ATTEMPT DID NOT FIX ANYTHING. I reached for _is_secret_setting; it
returns False for PWD, Password and sslpassword, because it matches a fixed
frozenset of SETTINGS names while these are ODBC DRIVER keywords with different
spellings and case. That fix would have masked nothing while reading as a fix,
inside the change closing a defect whose whole shape is a control whose domain is
narrower than its surface.

AND THE GUARD I WROTE AGAINST THIS CLASS WAS GREEN OVER IT. The domain test
filtered its AST-derived domain through _decorator_style, keeping 4 of 23 and
dropping every base constructor including Database. Its docstring asserted "no
shipped factory emits a nested container beyond those declared below" and called
the hole THEORETICAL. Both false. DELETED, not softened: a number a test has not
established has no business in the file defining the test.

Domain is now all 23 and its COVERAGE IS ASSERTED. Every other control in that
file answers "is this instrument working"; none answered "is it pointed at the
whole thing".

Found on the way: Http and Soap REFUSE an inline intake credential and demand
env(), so the value never reaches settings -- the stronger control odbc_params
lacks. Now asserted rather than folklore.

Mutation-proved: removing the descent reds Database and DatabasePoll. 103 tests
green across the redaction, connection-API and rotation suites.

Found by the asvs-tracking-rework session's independent assessment of ASVS 15.3.1,
which I recused from because I authored the fixes bearing on that cell.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant