Skip to content

fix(toolset): reject cmd.exe metacharacters in a Windows tool path - #11947

Merged
jdx merged 1 commit into
jdx:mainfrom
JamBalaya56562:fix/windows-tool-path-cmd-metachars
Aug 13, 2026
Merged

jdx merged 1 commit into
jdx:mainfrom
JamBalaya56562:fix/windows-tool-path-cmd-metachars

Conversation

@JamBalaya56562

@JamBalaya56562 JamBalaya56562 commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

The denylist that path: values are checked against is written for a POSIX shell. On Windows the
shell is cmd.exe, and its metacharacters pass straight through.

Background

#9814 added validate_path_string because a path: value
resolves into ctx.rootPath for path-mode tools, which vfox plugin hooks interpolate into shell
commands. It rejects $, backtick, quotes and \ — the characters that break a POSIX shell.

Those are not cmd's metacharacters. Measured on 2026.8.2 windows-x64 — every one of these is
accepted today:

path = "C:/a&b/tool"            accepted     ( & separates commands )
path = "C:/%USERPROFILE%/tool"  accepted     ( %NAME% expands )
path = "C:/a^b/tool"            accepted     ( ^ escapes )
path = "C:/a|b/tool"            accepted     ( | pipes )
path = "C:/a$b/tool"            REJECTED     <- the POSIX control, the only one that fires

% is the one that matters most. cmd expands %NAME% even inside double quotes, so a hook
that quotes its interpolation correctly still cannot contain it. The effect is that the path a
plugin ends up operating on is an environment variable's value rather than the string that was
written — I confirmed the expansion takes effect when a path-mode tool is set up, so this is not
theoretical. The others (& | < > ^) are cmd's command-separator / redirect / escape set, the
same class $ is on the POSIX side.

The change

On Windows, path: also rejects & | < > ^ %. This is #9814's rejection, mirrored onto the
platform where cmd is the shell — not a new kind of check.

fn is_forbidden_path_char(c: char) -> bool {
    is_forbidden_version_char(c) || is_forbidden_cmd_char(c)
}

#[cfg(windows)]
fn is_forbidden_cmd_char(c: char) -> bool {
    matches!(c, '&' | '|' | '<' | '>' | '^' | '%')
}

Scoped deliberately:

  • Windows only. #[cfg(windows)]; nothing changes off Windows.
  • path: only, not versions. ^1.2.3 is a real npm-style range, so ^ must not become a hard
    error in a version. The version denylist is untouched; only the new path:-specific
    is_forbidden_path_char adds the cmd set.

Nothing that worked before is rejected now

  • % — resolve_path only ever expanded ~/, never %VAR%, so a % in a path: was already
    non-functional. Rejecting it removes nothing that worked.
  • < > | — already invalid in a Windows filename, so a path containing them failed downstream in
    directory creation with a bare OS error. Rejecting at validation just turns that into a clear
    message.
  • & ^ — legal in a Windows path but very rare; rejecting is the hardening.

( ) are deliberately not on the list

They are cmd metacharacters in the sense that a ) can close a ( … ) block early, but they are
not an injection vector, which is the bar the rest of this list meets. Measured, with & as the
control:

> cmd /c "chmod +x C:\a&echo AMP-INJECTED"
AMP-INJECTED                                                 <- injected

> cmd /c "chmod +x C:\a(echo PAREN-INJECTED)b"
'chmod' is not recognized as an internal or external command <- nothing injected

The breakage case is real but needs a hook that both wraps the interpolation in a ( … ) block
and leaves the path unquoted; double-quoting contains it, and the bundled hooks that build a
Windows command line double-quote. Against that, C:\Program Files (x86)\… is one of the two
standard program directories on 64-bit Windows and exactly the kind of path path: exists to
point at — rejecting it would be a certain, common breakage traded for a hypothetical one.

Tests

path: had no Windows coverage before this.

  • unit — #[cfg(windows)]: each of &, %, ^, |, <, > in a path is rejected, and an
    ordinary C:/Program Files/tool still passes. All-platform: a version keeps accepting ^1.2.3
    and 1.0&x (the cmd set is path-only) while the POSIX $(id) stays rejected — the control that
    this widened the path list without touching versions. The module is not #[cfg(unix)], so CI
    runs the Windows cases.
  • e2e-win (new) — tool_path_metacharacters.Tests.ps1: %, &, ^, | paths make
    mise ls --current fail with invalid tool path, plus an ordinary path as the control.

docs/configuration.md's path: entry gains a sentence. It is hand-written, so nothing is
regenerated.

Not run

cargo fmt --all and an isolated rustc probe of the denylist; no full local build. The
measurements above are of a released binary. CI, and windows-e2e in particular, is what exercises
the change.

Overlap with #11937

Resolved — #11937 has merged and this branch is rebased onto it. The two changes compose as
expected: separators are rewritten first, then the value is validated against the POSIX+cmd set.
Merging them by hand kept both sides of three conflicts rather than picking one, since the TOML
quoting note and the cmd metacharacter note are about different things, and both files' tests are
still present.

Originally written as:

#11937 also edits validate_path_string — it rewrites
\ to / before validation so native Windows paths are accepted. This adds rejections inside
validation. They compose in that order (rewrite separators, then validate against the POSIX+cmd
set) and the hunks are adjacent but distinct; whichever lands second should rebase. The two
Windows path e2e files have different names on purpose.

Summary by CodeRabbit

  • Bug Fixes

    • Improved Windows path validation by rejecting shell metacharacters that could cause execution issues.
    • Preserved support for valid version specifications, including npm-style caret ranges.
    • Confirmed that standard Windows paths continue to work as expected.
    • Added coverage for invalid Windows paths containing %, &, ^, or |.
  • Documentation

    • Expanded configuration guidance with Windows-specific path validation details.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 95a6e505-58ce-47bd-8052-f227697af8c7

📥 Commits

Reviewing files that changed from the base of the PR and between a718f60 and 6477350.

📒 Files selected for processing (2)
  • docs/configuration.md
  • src/toolset/tool_request.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/configuration.md
  • src/toolset/tool_request.rs

📝 Walkthrough

Walkthrough

Windows path validation now rejects shell metacharacters, including Windows cmd.exe characters. Version validation remains POSIX-only. Unit tests, Windows end-to-end tests, and configuration documentation cover the behavior.

Changes

Windows tool path validation

Layer / File(s) Summary
Path validation and unit coverage
src/toolset/tool_request.rs
Path validation uses a dedicated denylist with Windows cmd.exe metacharacters. Version validation retains POSIX-only rules. Tests cover both behaviors.
Windows integration coverage and documentation
e2e-win/tool_path_metacharacters.Tests.ps1, docs/configuration.md
Windows end-to-end tests verify rejected metacharacter paths and accepted ordinary paths. The documentation describes the validation rule.

Estimated code review effort: 2 (Simple) | ~10 minutes

Mergeability Score: ⚪ Minimal · up to 64773

This change rejects Windows shell metacharacters in tool paths while preserving existing version behavior; no actionable merge-blocking risk remains beyond normal checks and review.

Possibly related PRs

  • jdx/mise#11937: Both changes update tool path validation and Windows-specific tests.
  • jdx/mise#11948: Both changes update Windows Pester handling of MISE_TRUSTED_CONFIG_PATHS.

Poem

A rabbit checks each Windows trail,
And blocks the symbols that derail.
% and & must stay away,
While normal paths pass the day.
Tests hop cleanly through the gate.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main Windows tool-path validation change.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR strengthens Windows path: validation by rejecting cmd.exe metacharacters before tool paths reach plugin-generated shell commands.

  • Adds a Windows-specific denylist while preserving existing version syntax.
  • Adds unit and Windows end-to-end coverage for rejected and ordinary paths.
  • Documents the Windows path restrictions.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/toolset/tool_request.rs Adds path-specific Windows metacharacter validation without changing the version-string denylist.
e2e-win/tool_path_metacharacters.Tests.ps1 Covers rejection of representative cmd.exe metacharacters and acceptance of an ordinary Windows path.
docs/configuration.md Documents the additional Windows path validation and its shell-command rationale.

Reviews (3): Last reviewed commit: "fix(toolset): reject cmd.exe metacharact..." | Re-trigger Greptile

Comment thread src/toolset/tool_request.rs Resolved
@JamBalaya56562
JamBalaya56562 marked this pull request as draft August 13, 2026 18:16
`path:` values are checked against a denylist because the resolved path
becomes `ctx.rootPath` for path-mode tools, which vfox plugin hooks build
shell commands with. That list was added by jdx#9814 for a POSIX shell, so it
covers `$`, backtick, quotes and `\`. On Windows the shell those hooks run
through is cmd.exe, whose metacharacters are a different set and passed
straight through: `&`, `|`, `<`, `>`, `^`, and `%`.

`%` is the one that matters most. cmd expands `%NAME%` even inside double
quotes, so a hook that quotes its interpolation correctly still cannot
contain it -- the path a plugin operates on ends up being an environment
variable's value rather than what was written. Measured: a `path:` value
containing `%USERPROFILE%` resolves to the expanded location.

Rejected on Windows only, and only for `path:`. Versions keep the POSIX-only
list, since `^1.2.3` is a real npm-style range that must not become a hard
error. `resolve_path` never expanded `%VAR%` in a path, so nothing that
worked before is rejected now.

`path:` had no e2e coverage on Windows before this.
@JamBalaya56562
JamBalaya56562 force-pushed the fix/windows-tool-path-cmd-metachars branch from a718f60 to 6477350 Compare August 13, 2026 19:37
@JamBalaya56562
JamBalaya56562 marked this pull request as ready for review August 13, 2026 20:19
@JamBalaya56562
JamBalaya56562 marked this pull request as draft August 13, 2026 20:24
@JamBalaya56562
JamBalaya56562 marked this pull request as ready for review August 13, 2026 20:41
@jdx
jdx merged commit 1859f9c into jdx:main Aug 13, 2026
31 checks passed
@JamBalaya56562
JamBalaya56562 deleted the fix/windows-tool-path-cmd-metachars branch August 13, 2026 21:06
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.

2 participants