fix(cli): disable click Windows argv expansion so secret values are stored byte-for-byte (KSM-1186) - #1102
Conversation
…tored byte-for-byte (KSM-1186) click >= 8.0 expands every command-line argument on Windows before parsing (BaseCommand.main -> _expand_args: os.path.expanduser, os.path.expandvars, then glob). ntpath.expandvars expands %VAR%/$VAR and collapses $$ to $, so 'password=a$$b' was stored as 'a$b' and 'login=x%OS%y' as 'xWindows_NTy' - silent data corruption of stored secrets, plus premature %VAR% expansion inside 'ksm exec' command arguments. Windows-only; pip and frozen surfaces. Pass windows_expand_args=False (the click 8.0.1+ opt-out) in main()'s group invocation, bump the click floor accordingly, and add a wiring test (CliRunner bypasses BaseCommand.main(), which is how this evaded the unit suite - the test asserts the opt-out is passed).
caa77ba to
4d8c192
Compare
There was a problem hiding this comment.
I checked the fix against the click 8.0.1 source. windows_expand_args is the correct opt-out, and click gates it to os.name == "nt". The new test is a solid wiring test. Good stuff!
One thing: setup.py bumps click>=8.0 to click>=8.0.1. The sibling file integration/keeper_secrets_manager_cli/requirements.txt still pins click>=8.0. I installed click 8.0.0 and called cli(obj={}, prog_name='ksm', windows_expand_args=False). This raised TypeError: Context.__init__() got an unexpected keyword argument 'windows_expand_args'. Please bump requirements.txt to click>=8.0.1 too.
Great catch! Will do, thank you! |
…(KSM-1186) Review follow-up: requirements.txt still allowed click 8.0.0, which lacks the windows_expand_args parameter and raises TypeError when main() passes it. Both dependency declarations now require 8.0.1+.
|
Addressed: |
Bug
KSM-1186 — on Windows, click ≥ 8.0 expands every command-line argument before parsing (
BaseCommand.main()defaultswindows_expand_args=True→_expand_args():os.path.expanduser, thenos.path.expandvars, then glob).ntpath.expandvarsexpands%VAR%/$VARand collapses$$→$, so secret values passed as arguments were silently corrupted before storage:password=a$$ba$blogin=x%OS%yxWindows_NTyWindows-only (posix never collapses
$$; click gates the expansion onos.name == "nt"), affects every click argument (field values, titles, notes,sync --map, …) on both pip and frozen surfaces, and also pre-expands%VAR%insideksm execcommand arguments so the documented Windows exec pattern delivered raw notation instead of the resolved secret. Pre-existing — reproduced byte-identically on 1.4.0 — not a 1.5.0 regression.Fix
__main__.pymain(): passwindows_expand_args=False(the click 8.0.1+ opt-out) in the group invocation — secret values are not shell globs.setup.py: bumpclick>=8.0→click>=8.0.1(the version that introduced the opt-out parameter).tests/windows_expand_args_test.py(new): assertsmain()passes the opt-out.click.testing.CliRunnerbypassesBaseCommand.main()entirely — exactly how this bug evaded the unit suite — so the wiring is the honest unit-testable surface; byte fidelity end-to-end is covered by the release regression suite driving the packaged CLI in a real subprocess.ksm shellinner commands dispatch through click-repl'smake_context/invoke, neverBaseCommand.main(), so the KSM-1162/1165 shell tokenizer paths are untouched.Verification (Windows 11, live QA US vault, editable install of this branch)
a$$b,x%OS%y, glob*,~admin, and undefined-var controls all stored byte-for-byte onsecret add fieldandsecret update(each corrupted pre-fix except the controls).ksm exec -- cmd /c "echo RESOLVED=%MYPW%"now delivers the resolved secret to the child (pre-fix: raw notation).main()(verified via stash), passes patched.test_symlink_rejection; no regressions.Full QA test plan (13 test cases) and acceptance criteria are on KSM-1186 (comment of 2026-08-12). Note for release planning: the current 1.5.0 publish artifacts (run 31418361581) do not contain this fix; the frozen surface needs re-verification on whichever rebuilt installer ships it.