Repository navigation
fix(execution): refuse a relative configured command - #342
Conversation
wslc.command and compose.command come from the project's wip.yml. A relative path was resolved against the directory wip ran from, so a cloned repository could point wip at a binary inside itself. Accept only a bare name, searched on PATH, or a fully qualified path; drive- and root-relative forms are refused too. Closes #338 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
IsPathFullyQualified accepts \\server\share\wslc.exe, so a wip.yml could run a binary over SMB without knowing where it was cloned. On Windows a configured path must now have a drive root; a mapped drive stays allowed. Also reword the relative-path refusal so the "compose command" label no longer prints "command command". Refs #338 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes #338.
CommandResolver.Resolveturned a configured relative command into an absolute path by resolving it against the process's current directory, so awip.ymlcommitted to a repository someone had just cloned could point wip at a binary inside that repository and have it run with their privileges. #329 closed the launcher side; this path defeated that guard rather than tripping it, because the resolver had already absolutized the value before the guard saw it.The rule
A configured
wslc.command/compose.commandmust be a bare name, searched on PATH, or a fully qualified path. Anything else is refused before any resolver sees it.This is what the setting is already documented to do — the
wslc:block listsauto, "any other name: must be executable onPATH", and "an absolute path". A relative path was never documented, so this brings the code to the page rather than changing documented behaviour.Three details:
resolveExecutable, so an injected resolver cannot route around it. Covered by a test.Path.IsPathFullyQualified, notIsPathRooted.IsPathRootedaccepts the drive-relativeC:wslc.exeand the root-relative\wslc.exe, which still depend on the current drive or directory.IsPathRootedis used only to detect that a separator-less value likeC:wslc.exeis a path at all — as a bare name it would otherwise reach the PATH walk and come back as itself.CommandNotFoundExceptionis reused, keeping it the only failure callers have to handle, as that file's contract states. The message carries the meaning: the value, the rule, and why.The
candidatesloop is untouched. Candidates are code-supplied (DefaultCandidates,["git"]inRulesFetcher), neverwip.ymlcontent.Tests
ExplicitRelativePathIsFrozenAsAbsolutePathasserted the behaviour being removed, so its setup became the refusal test. Added: the absolute path still resolving, a bare name still resolving on PATH, the drive- and root-relative forms, and the injected-resolver case.UnnormalizableCandidate…now uses an absolute path so it still exercises normalization rather than the new guard.Each refusal test puts a real file where the value points, so only the guard can make it throw — not an absent file.
Verification
On the Windows host, SDK 10.0.403:
dotnet restore wip.slnx --locked-mode— up to date, no lock file changeddotnet build wip.slnx --configuration Release— 0 warnings, 0 errorsdotnet test— 1746 total, 0 failed, 1651 passed, 95 skippedCommandResolverTestsalone — 16 total, 0 failed, 0 skipped, so the Windows-only cases really ranMutation check: with the
RejectRelativePathcall commented out, 7 cases in that class fail (the refusal test, all four drive/root-relative forms, and both injected-resolver forms). The tests fail without the fix.The same change was also run on Linux during implementation: 1746 total, 0 failed, 10 skipped — there the four Windows-only cases skip, which is why they were re-run here.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LhR19gpMx4ZBuq6pkP4fgY
Summary by CodeRabbit
PATH.