From b6a8f2453fd69121498e4c7051b4de51ebe0a829 Mon Sep 17 00:00:00 2001 From: Adolanium <94890352+Adolanium@users.noreply.github.com> Date: Mon, 24 Aug 2026 07:15:44 +0300 Subject: [PATCH 1/3] Quote mise-install arguments and refuse unusable command names The wrapper is written through an unquoted heredoc, so the package and bin names land in it as shell source. A package name carrying shell characters became code that ran every time the wrapper ran. The command name is used raw as a file name under ~/.local/bin, so a slash in it wrote and removed somewhere else entirely. Quote both values with printf %q, and refuse command names that are not plain file names before anything is removed or written. --- bin/omarchy-mise-install | 23 ++++++++- test/shell.d/mise-install-test.sh | 84 +++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 test/shell.d/mise-install-test.sh diff --git a/bin/omarchy-mise-install b/bin/omarchy-mise-install index e6dfc96c..8bec3f31 100755 --- a/bin/omarchy-mise-install +++ b/bin/omarchy-mise-install @@ -12,8 +12,27 @@ package=$1 command=${2:-$1} bin=${3:-$command} +# The command name becomes a file name under ~/.local/bin, so a slash in it +# writes the wrapper somewhere else and the rm below deletes somewhere else. A +# leading dot hides it or walks up, and a leading dash makes a name that reads +# as an option to whatever picks it up. Checked before anything is removed or +# written, and kept to those shapes so package names like npm:playwright still +# stand in for the command name. +case "$command" in + */* | .* | -* | *[[:cntrl:]]*) + echo "omarchy-mise-install: '$command' is not usable as a command name" >&2 + exit 1 + ;; +esac + mkdir -p "$HOME/.local/bin" +# The heredoc below is unquoted, so whatever these hold is written into the +# wrapper as shell source. Quote them the way omarchy-install-and-launch does, so +# a package name carrying shell characters stays one argument instead of running. +printf -v package_arg '%q' "$package" +printf -v bin_arg '%q' "$bin" + # These tools install and upgrade on first run, so mise's release cooldown would # hold a new version back for days after it ships. Exported rather than set on # the install line alone, so resolving the version to execute agrees with the @@ -22,8 +41,8 @@ rm -f "$HOME/.local/bin/$command" cat >"$HOME/.local/bin/$command" <"$stub_bin/mise" <<'SH' +#!/bin/bash + +printf 'mise' >>"$OMARCHY_MISE_TEST_LOG" +for arg in "$@"; do + printf '\t%s' "$arg" >>"$OMARCHY_MISE_TEST_LOG" +done +printf '\n' >>"$OMARCHY_MISE_TEST_LOG" +SH +chmod +x "$stub_bin/mise" + +install_wrapper() { + HOME="$home" "$ROOT/bin/omarchy-mise-install" "$@" +} + +# The ordinary case still works, and every call site in install/user/mise.sh +# passes names of this shape. +install_wrapper npm:playwright playwright >/dev/null +[[ -x $home/.local/bin/playwright ]] || + fail "a normal install writes an executable wrapper" + +log="$tmpdir/normal.log" +: >"$log" +OMARCHY_MISE_TEST_LOG="$log" PATH="$stub_bin:$PATH" "$home/.local/bin/playwright" >/dev/null +grep -Fqx $'mise\tuse\t-g\t--quiet\tnpm:playwright' "$log" || + fail "the wrapper asks mise for the package it was given" "$(cat "$log")" + +pass "a normal install writes a wrapper that names its package" + +# A package name is data. Quoted with %q it reaches mise as one argument +# instead of being read as shell source when the wrapper runs. +install_wrapper 'npm:pkg$(touch '"$tmpdir"'/PWNED)end' hostile >/dev/null + +log="$tmpdir/hostile.log" +: >"$log" +OMARCHY_MISE_TEST_LOG="$log" PATH="$stub_bin:$PATH" "$home/.local/bin/hostile" >/dev/null + +[[ -e $tmpdir/PWNED ]] && + fail "a package name with shell characters does not run when the wrapper does" \ + "wrapper: $(cat "$home/.local/bin/hostile")" + +grep -Fqx $'mise\tuse\t-g\t--quiet\tnpm:pkg$(touch '"$tmpdir"'/PWNED)end' "$log" || + fail "the package reaches mise whole" "$(cat "$log")" + +pass "a package name with shell characters reaches mise as one argument" + +# The command name is a file name under ~/.local/bin. These shapes escape it, +# hide it, or make something that reads as an option. +for name in ../escaped .hidden -dash; do + if install_wrapper somepkg "$name" >/dev/null 2>"$tmpdir/err"; then + fail "a command name of '$name' is refused" + fi + grep -Fq 'is not usable as a command name' "$tmpdir/err" || + fail "the refusal says why for '$name'" "$(cat "$tmpdir/err")" +done + +pass "command names that are not plain file names are refused" + +# The refusal has to land before the rm, which would otherwise delete the +# escaped path on its way to failing. +victim="$tmpdir/victim" +printf 'keep me\n' >"$victim" +if install_wrapper somepkg "../../../..$victim" >/dev/null 2>&1; then + fail "an escaping command name is refused" +fi +[[ -f $victim ]] || + fail "an escaping command name removes nothing outside ~/.local/bin" + +pass "an escaping command name removes nothing outside ~/.local/bin" From e090d01877ebd3bf172d26cb5c8b62b1dcd14b76 Mon Sep 17 00:00:00 2001 From: Adolanium <94890352+Adolanium@users.noreply.github.com> Date: Mon, 24 Aug 2026 07:34:36 +0300 Subject: [PATCH 2/3] Cover the control-character branch in the mise-install guard The refusal table exercised slash, leading dot and leading dash but never a control character, so that branch of the guard could have been dropped without a test noticing. Add a newline and a tab case, labelled so the value stays out of the output. --- test/shell.d/mise-install-test.sh | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/test/shell.d/mise-install-test.sh b/test/shell.d/mise-install-test.sh index edebd05a..951db41d 100644 --- a/test/shell.d/mise-install-test.sh +++ b/test/shell.d/mise-install-test.sh @@ -60,13 +60,26 @@ grep -Fqx $'mise\tuse\t-g\t--quiet\tnpm:pkg$(touch '"$tmpdir"'/PWNED)end' "$log" pass "a package name with shell characters reaches mise as one argument" # The command name is a file name under ~/.local/bin. These shapes escape it, -# hide it, or make something that reads as an option. -for name in ../escaped .hidden -dash; do +# hide it, make something that reads as an option, or carry characters that have +# no business in a file name. Labelled so a newline in the value does not end up +# inside the test output. +refused=( + "a slash" "../escaped" + "a leading dot" ".hidden" + "a leading dash" "-dash" + "a newline" $'with\nnewline' + "a tab" $'with\ttab' +) + +for (( i = 0; i < ${#refused[@]}; i += 2 )); do + label=${refused[i]} + name=${refused[i + 1]} + if install_wrapper somepkg "$name" >/dev/null 2>"$tmpdir/err"; then - fail "a command name of '$name' is refused" + fail "a command name with $label is refused" fi grep -Fq 'is not usable as a command name' "$tmpdir/err" || - fail "the refusal says why for '$name'" "$(cat "$tmpdir/err")" + fail "the refusal says why for a command name with $label" "$(cat "$tmpdir/err")" done pass "command names that are not plain file names are refused" From a7a4a682e530d257b1a149589381946822bc64ab Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 7 Sep 2026 02:24:40 -0400 Subject: [PATCH 3/3] Keep mise cleanup compatible with escaped wrappers Co-Authored-By: GPT-5.6 Sol --- bin/omarchy-mise-install | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bin/omarchy-mise-install b/bin/omarchy-mise-install index 8bec3f31..356c3324 100755 --- a/bin/omarchy-mise-install +++ b/bin/omarchy-mise-install @@ -33,6 +33,17 @@ mkdir -p "$HOME/.local/bin" printf -v package_arg '%q' "$package" printf -v bin_arg '%q' "$bin" +# Keep values that are inert inside double quotes in the form existing migrations +# recognize, including mise backend options that %q would unnecessarily escape. +case "$package" in + *'$'* | *'`'* | *'"'* | *'\'* | *'!'* | *[[:cntrl:]]*) ;; + *) package_arg="\"$package\"" ;; +esac +case "$bin" in + *'$'* | *'`'* | *'"'* | *'\'* | *'!'* | *[[:cntrl:]]*) ;; + *) bin_arg="\"$bin\"" ;; +esac + # These tools install and upgrade on first run, so mise's release cooldown would # hold a new version back for days after it ships. Exported rather than set on # the install line alone, so resolving the version to execute agrees with the