fix(config): redact credentials inside odbc_params, and widen the guard's domain (#1206) - #305
Open
wshallwshall wants to merge 1 commit into
Open
fix(config): redact credentials inside odbc_params, and widen the guard's domain (#1206)#305wshallwshall wants to merge 1 commit into
wshallwshall wants to merge 1 commit into
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
redacted_settingsmasked flat scalars and descended intoheadersalone, so a credential inodbc_paramswas returned verbatim byGET /connections/{name}/metadatabehindMONITORING_READand printed bygraph --json— on the same object whose top-levelpasswordmasked correctly.Measured, both serializers
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_paramsrefusesenv()there. So a connection needing a second driver credential — libpqsslpasswordbesidePWD— has no typed home and noenv()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 returnsFalsefor every one ofPWD,Passwordandsslpassword— 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_keyis shape-based and case-insensitive;pwdis 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.pyfiltered its AST-derived domain through_decorator_style, keeping 4 of 23 spec-returning functions and dropping every base constructor includingDatabase. 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_functionfails 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
HttpandSoaprefuse an inline intake credential outright and demandenv(), so the value never resolves into settings and no serializer can leak it. That is the stronger controlodbc_paramslacks, and it is now asserted bytest_a_refusing_connector_actually_refuses_an_inline_credentialrather than left as folklore.Verification
DatabaseandDatabasePoll.signing_keyis a path (direct.py_read_files it, and the engine classifies the passphrase and not the key deliberately);odbc_password_key/odbc_user_keyare keyword names, not credentials.Provenance
Found by the parallel
asvs-tracking-reworksession'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.