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] 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"