Remove unsafe project bin PATH injection (#11336)
* Remove unsafe project bin PATH injection * Cover customized unsafe Mise paths * Revoke legacy Mise Work trust * Harden legacy Mise trust cleanup * Preserve ignored Mise Work configs * Scope Mise path cleanup to env * Accept paranoid Mise ignore marker Reported-by: infosec-us-team
This commit is contained in:
1 parent
b44fb74780
commit
a73bcbfc0a
4 files changed
+371
-7
No files matched your search
@@ -2,13 +2,6 @@
|
||||
mkdir -p "$HOME/Work"
|
||||
mkdir -p "$HOME/Work/tries"
|
||||
|
||||
cat >"$HOME/Work/.mise.toml" <<'EOF'
|
||||
[env]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
EOF
|
||||
|
||||
mise trust ~/Work/.mise.toml
|
||||
|
||||
# Offline installs unpack the Node tarball bundled by the ISO: from
|
||||
# /opt/packages in the ISO chroot, or from the copy staged in provisioning state when
|
||||
# omarchy-provision-owner finalizes the user at first boot.
|
||||
|
||||
@@ -57,3 +57,5 @@ The full manual can be found via `man yt-dlp`.
|
||||
## try
|
||||
|
||||
[try](https://github.com/tobi/try) makes it easy to manage programming experiments with date-stamped directories. All experiments live in `~/Work/tries` and you can access them via `try`.
|
||||
|
||||
Omarchy does not add a project's `bin/` directory to `PATH` automatically. Run trusted project-local tools with an explicit relative path, such as `bin/rails` or `./bin/dev`.
|
||||
@@ -0,0 +1,99 @@
|
||||
echo "Remove automatic project bin directories from PATH"
|
||||
|
||||
work_dir="$HOME/Work"
|
||||
mise_config="$work_dir/.mise.toml"
|
||||
# install/user/mise-work.sh as shipped in Omarchy 4.0.3.
|
||||
stock_sha="bd04f191d63bbde86920f44f76f0989fad980afc84e268e8474c201ec7149245"
|
||||
cwd_bin='\{\{[[:space:]]*cwd[[:space:]]*\}\}/bin'
|
||||
unsafe_path="^[[:space:]]*_[.]path[[:space:]]*=[[:space:]]*(\"$cwd_bin\"|'$cwd_bin')[[:space:]]*(#.*)?$"
|
||||
env_section='^[[:space:]]*\[[[:space:]]*env[[:space:]]*\][[:space:]]*(#.*)?$'
|
||||
any_section='^[[:space:]]*\[\[?.*\]\]?[[:space:]]*(#.*)?$'
|
||||
|
||||
remove_empty_work_dir=false
|
||||
if [[ ! -e $work_dir ]]; then
|
||||
mkdir -p "$work_dir"
|
||||
remove_empty_work_dir=true
|
||||
fi
|
||||
|
||||
was_ignored=false
|
||||
if [[ -d $work_dir ]]; then
|
||||
mise_state_dir=${MISE_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/mise}
|
||||
ignored_configs_dir="$mise_state_dir/ignored-configs"
|
||||
work_target=$(readlink -m "$work_dir")
|
||||
config_path_target="$work_target/.mise.toml"
|
||||
config_target=$(readlink -m "$mise_config")
|
||||
|
||||
if [[ -d $ignored_configs_dir ]]; then
|
||||
for ignored_entry in "$ignored_configs_dir"/*; do
|
||||
[[ -L $ignored_entry ]] || continue
|
||||
ignored_target=$(readlink "$ignored_entry")
|
||||
if [[ $ignored_target == $work_target || $ignored_target == $config_path_target || $ignored_target == $config_target ]]; then
|
||||
was_ignored=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ $was_ignored == "false" ]]; then
|
||||
# Normal Mise trust is recorded against the config-root directory, while
|
||||
# paranoid trust is recorded against the file and its contents. Stage an
|
||||
# empty, inert config when the legacy file is gone so either trust mode can
|
||||
# resolve and revoke the original grant.
|
||||
remove_empty_mise_config=false
|
||||
if [[ ! -e $mise_config && ! -L $mise_config ]]; then
|
||||
if (set -o noclobber; : >"$mise_config") 2>/dev/null; then
|
||||
remove_empty_mise_config=true
|
||||
fi
|
||||
fi
|
||||
|
||||
untrust_target="$work_dir"
|
||||
if [[ -f $mise_config ]]; then
|
||||
untrust_target="$mise_config"
|
||||
fi
|
||||
|
||||
if mise trust --untrust "$untrust_target"; then
|
||||
:
|
||||
else
|
||||
if [[ $remove_empty_mise_config == "true" ]]; then
|
||||
rm -f -- "$mise_config"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $remove_empty_mise_config == "true" ]]; then
|
||||
rm -f -- "$mise_config"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f $mise_config ]]; then
|
||||
if [[ ! -L $mise_config && $(sha256sum "$mise_config" | cut -d ' ' -f 1) == $stock_sha ]]; then
|
||||
rm -f -- "$mise_config"
|
||||
else
|
||||
unsafe_env_paths=$(sed -n -E "\\%$env_section%,\\%$any_section% { \\%$unsafe_path%p; }" "$mise_config")
|
||||
if [[ -n $unsafe_env_paths ]]; then
|
||||
backup=$(mktemp "$mise_config.bak.XXXXXX")
|
||||
cp -p -- "$mise_config" "$backup"
|
||||
sed --follow-symlinks -i -E "\\%$env_section%,\\%$any_section% { \\%$unsafe_path%d; }" "$mise_config"
|
||||
|
||||
printf '\n%s\n' \
|
||||
"Automatic project bin directories were removed from your Mise PATH." \
|
||||
"Your other Mise settings were preserved."
|
||||
printf '\nBackup saved to:\n %s\n' "$backup"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f $mise_config ]]; then
|
||||
if [[ $was_ignored == "true" ]]; then
|
||||
printf '\n%s\n' "This custom config remains ignored by Mise."
|
||||
else
|
||||
printf '\n%s\n %s\n' \
|
||||
"Mise trust for this custom config was revoked. Review it before trusting it again:" \
|
||||
"mise trust $mise_config"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $remove_empty_work_dir == "true" ]]; then
|
||||
rmdir "$work_dir" 2>/dev/null || true
|
||||
fi
|
||||
@@ -0,0 +1,270 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
require_command mise
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
migration="$ROOT/migrations/1789095456.sh"
|
||||
|
||||
run_migration() {
|
||||
local test_home="$1"
|
||||
|
||||
env -i \
|
||||
HOME="$test_home" \
|
||||
XDG_CACHE_HOME="$test_home/.cache" \
|
||||
XDG_CONFIG_HOME="$test_home/.config" \
|
||||
XDG_DATA_HOME="$test_home/.local/share" \
|
||||
XDG_STATE_HOME="$test_home/.local/state" \
|
||||
MISE_PARANOID="${OMARCHY_TEST_MISE_PARANOID:-false}" \
|
||||
PATH=/usr/bin \
|
||||
bash -euo pipefail "$migration"
|
||||
}
|
||||
|
||||
run_mise() {
|
||||
local test_home="$1"
|
||||
shift
|
||||
|
||||
env -i \
|
||||
HOME="$test_home" \
|
||||
XDG_CACHE_HOME="$test_home/.cache" \
|
||||
XDG_CONFIG_HOME="$test_home/.config" \
|
||||
XDG_DATA_HOME="$test_home/.local/share" \
|
||||
XDG_STATE_HOME="$test_home/.local/state" \
|
||||
MISE_PARANOID="${OMARCHY_TEST_MISE_PARANOID:-false}" \
|
||||
PATH=/usr/bin \
|
||||
mise "$@"
|
||||
}
|
||||
|
||||
mise_environment() {
|
||||
local test_home="$1"
|
||||
local project="$2"
|
||||
|
||||
(
|
||||
cd "$project"
|
||||
run_mise "$test_home" env -s bash
|
||||
)
|
||||
}
|
||||
|
||||
mise_path_active() {
|
||||
local test_home="$1"
|
||||
local project="$2"
|
||||
local output
|
||||
|
||||
if ! output=$(mise_environment "$test_home" "$project" 2>/dev/null); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
grep -F "$project/bin" <<<"$output" >/dev/null
|
||||
}
|
||||
|
||||
assert_unsafe_variant_removed() {
|
||||
local variant="$1"
|
||||
local assignment="$2"
|
||||
local variant_home="$test_dir/$variant-home"
|
||||
local variant_config="$variant_home/Work/.mise.toml"
|
||||
local variant_project="$variant_home/Work/tries/untrusted-repository"
|
||||
|
||||
mkdir -p "$variant_project/bin"
|
||||
printf '[env]\n%s\n' "$assignment" >"$variant_config"
|
||||
run_mise "$variant_home" trust "$variant_config" >/dev/null
|
||||
|
||||
mise_path_active "$variant_home" "$variant_project" || fail "$variant legacy config prepends the repository bin directory"
|
||||
|
||||
run_migration "$variant_home" >/dev/null
|
||||
if mise_path_active "$variant_home" "$variant_project"; then
|
||||
fail "$variant repository bin directory remains in PATH after migration"
|
||||
fi
|
||||
}
|
||||
|
||||
install_home="$test_dir/install-home"
|
||||
install_log="$test_dir/install-mise.log"
|
||||
mkdir -p "$install_home" "$test_dir/bin"
|
||||
cat >"$test_dir/bin/mise" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"$MISE_TEST_LOG"
|
||||
SH
|
||||
chmod +x "$test_dir/bin/mise"
|
||||
|
||||
env \
|
||||
HOME="$install_home" \
|
||||
MISE_TEST_LOG="$install_log" \
|
||||
OMARCHY_SETUP_CONTEXT=runtime \
|
||||
PATH="$test_dir/bin:/usr/bin" \
|
||||
bash -euo pipefail -c 'source "$1"' bash "$ROOT/install/user/mise-work.sh"
|
||||
|
||||
[[ -d $install_home/Work/tries ]] || fail "installer creates the work and tries directories"
|
||||
[[ ! -e $install_home/Work/.mise.toml ]] || fail "installer does not create a trusted Work Mise config"
|
||||
[[ $(<"$install_log") == "use -g node@latest" ]] || fail "installer only invokes Mise for the global Node setup"
|
||||
pass "new installs do not add project bin directories to PATH"
|
||||
|
||||
stock_home="$test_dir/stock-home"
|
||||
stock_config="$stock_home/Work/.mise.toml"
|
||||
stock_project="$stock_home/Work/tries/untrusted-repository"
|
||||
mkdir -p "$stock_project/bin"
|
||||
cat >"$stock_config" <<'TOML'
|
||||
[env]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
TOML
|
||||
run_mise "$stock_home" trust "$stock_config" >/dev/null
|
||||
|
||||
mise_path_active "$stock_home" "$stock_project" || fail "legacy config prepends the repository bin directory"
|
||||
|
||||
run_migration "$stock_home" >/dev/null
|
||||
[[ ! -e $stock_config ]] || fail "migration removes the stock Work Mise config"
|
||||
cat >"$stock_config" <<'TOML'
|
||||
[env]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
TOML
|
||||
if mise_path_active "$stock_home" "$stock_project"; then
|
||||
fail "recreated Work config remains trusted after migration"
|
||||
fi
|
||||
run_migration "$stock_home" >/dev/null
|
||||
[[ ! -e $stock_config ]] || fail "stock migration is idempotent"
|
||||
pass "migration removes the repository bin directory and revokes the Work trust root"
|
||||
|
||||
assert_unsafe_variant_removed inline-comment '_.path = "{{ cwd }}/bin" # Omarchy default'
|
||||
assert_unsafe_variant_removed single-quoted "_.path = '{{ cwd }}/bin'"
|
||||
pass "migration removes annotated and single-quoted project bin paths"
|
||||
|
||||
custom_home="$test_dir/custom-home"
|
||||
custom_config="$custom_home/Work/.mise.toml"
|
||||
mkdir -p "$(dirname "$custom_config")"
|
||||
cat >"$custom_config" <<'TOML'
|
||||
[env]
|
||||
KEEP = "yes"
|
||||
_.path = "{{ cwd }}/bin"
|
||||
# _.path = "{{ cwd }}/bin"
|
||||
|
||||
[tools]
|
||||
ruby = "latest"
|
||||
|
||||
[other]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
TOML
|
||||
cp "$custom_config" "$test_dir/custom-original"
|
||||
cat >"$test_dir/custom-expected" <<'TOML'
|
||||
[env]
|
||||
KEEP = "yes"
|
||||
# _.path = "{{ cwd }}/bin"
|
||||
|
||||
[tools]
|
||||
ruby = "latest"
|
||||
|
||||
[other]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
TOML
|
||||
chmod 600 "$custom_config"
|
||||
run_mise "$custom_home" trust "$custom_config" >/dev/null
|
||||
|
||||
custom_output=$(run_migration "$custom_home")
|
||||
cmp -s "$test_dir/custom-expected" "$custom_config" || fail "migration preserves unrelated custom Mise settings"
|
||||
[[ $(stat -c %a "$custom_config") == "600" ]] || fail "migration preserves custom config permissions"
|
||||
grep -F "mise trust $custom_config" <<<"$custom_output" >/dev/null || fail "migration explains how to review and re-trust a custom config"
|
||||
custom_backups=("$custom_config".bak.*)
|
||||
[[ -f ${custom_backups[0]} ]] || fail "migration backs up a customized Mise config"
|
||||
(( ${#custom_backups[@]} == 1 )) || fail "migration creates one custom config backup"
|
||||
cmp -s "$test_dir/custom-original" "${custom_backups[0]}" || fail "custom config backup preserves the original"
|
||||
|
||||
run_migration "$custom_home" >/dev/null
|
||||
custom_backups=("$custom_config".bak.*)
|
||||
(( ${#custom_backups[@]} == 1 )) || fail "custom migration does not create another backup on rerun"
|
||||
cmp -s "$test_dir/custom-expected" "$custom_config" || fail "custom migration is idempotent"
|
||||
pass "custom Mise settings, permissions, and original backup survive the repair"
|
||||
|
||||
unrelated_home="$test_dir/unrelated-home"
|
||||
unrelated_config="$unrelated_home/Work/.mise.toml"
|
||||
unrelated_project="$unrelated_home/Work/tries/untrusted-repository"
|
||||
mkdir -p "$unrelated_project/bin"
|
||||
printf '[env]\nKEEP = "yes"\n' >"$unrelated_config"
|
||||
cp "$unrelated_config" "$test_dir/unrelated-original"
|
||||
run_mise "$unrelated_home" trust "$unrelated_config" >/dev/null
|
||||
run_migration "$unrelated_home" >/dev/null
|
||||
cmp -s "$test_dir/unrelated-original" "$unrelated_config" || fail "unrelated Mise config remains unchanged"
|
||||
unrelated_backups=("$unrelated_config".bak.*)
|
||||
[[ ! -e ${unrelated_backups[0]} ]] || fail "unchanged Mise config is not backed up"
|
||||
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$unrelated_config"
|
||||
if mise_path_active "$unrelated_home" "$unrelated_project"; then
|
||||
fail "safe Work config retains its old trust grant"
|
||||
fi
|
||||
|
||||
absent_home="$test_dir/absent-home"
|
||||
absent_config="$absent_home/Work/.mise.toml"
|
||||
absent_project="$absent_home/Work/tries/untrusted-repository"
|
||||
mkdir -p "$(dirname "$absent_config")"
|
||||
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$absent_config"
|
||||
run_mise "$absent_home" trust "$absent_config" >/dev/null
|
||||
rm "$absent_config"
|
||||
rmdir "$absent_home/Work"
|
||||
run_migration "$absent_home" >/dev/null
|
||||
[[ ! -e $absent_home/Work ]] || fail "migration does not retain a temporary Work directory"
|
||||
mkdir -p "$absent_project/bin"
|
||||
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$absent_config"
|
||||
if mise_path_active "$absent_home" "$absent_project"; then
|
||||
fail "deleted Work directory retains its stale trust grant"
|
||||
fi
|
||||
pass "migration leaves unrelated configs alone and revokes dangling Work trust"
|
||||
|
||||
paranoid_home="$test_dir/paranoid-home"
|
||||
paranoid_work="$paranoid_home/Work"
|
||||
paranoid_config="$paranoid_work/.mise.toml"
|
||||
paranoid_project="$paranoid_work/tries/untrusted-repository"
|
||||
mkdir -p "$paranoid_project/bin"
|
||||
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$paranoid_config"
|
||||
OMARCHY_TEST_MISE_PARANOID=true run_mise "$paranoid_home" trust "$paranoid_config" >/dev/null
|
||||
OMARCHY_TEST_MISE_PARANOID=true mise_path_active "$paranoid_home" "$paranoid_project" || fail "paranoid legacy config prepends the repository bin directory"
|
||||
rm -r "$paranoid_work"
|
||||
|
||||
OMARCHY_TEST_MISE_PARANOID=true run_migration "$paranoid_home" >/dev/null
|
||||
[[ ! -e $paranoid_work ]] || fail "paranoid migration removes its temporary Work directory"
|
||||
mkdir -p "$paranoid_project/bin"
|
||||
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$paranoid_config"
|
||||
if OMARCHY_TEST_MISE_PARANOID=true mise_path_active "$paranoid_home" "$paranoid_project"; then
|
||||
fail "paranoid migration retains content-bound trust for the deleted legacy config"
|
||||
fi
|
||||
pass "migration revokes stale content-bound trust in Mise paranoid mode"
|
||||
|
||||
ignored_home="$test_dir/ignored-home"
|
||||
ignored_config="$ignored_home/Work/.mise.toml"
|
||||
ignored_project="$ignored_home/Work/tries/untrusted-repository"
|
||||
mkdir -p "$ignored_project/bin"
|
||||
cat >"$ignored_config" <<'TOML'
|
||||
[env]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
KEEP = "yes"
|
||||
TOML
|
||||
run_mise "$ignored_home" trust "$ignored_config" >/dev/null
|
||||
run_mise "$ignored_home" trust --ignore "$ignored_config" >/dev/null
|
||||
|
||||
ignored_output=$(run_migration "$ignored_home")
|
||||
grep -F '{{ cwd }}/bin' "$ignored_config" >/dev/null && fail "ignored config retains the unsafe path"
|
||||
grep -Fx 'KEEP = "yes"' "$ignored_config" >/dev/null || fail "ignored config keeps unrelated settings"
|
||||
ignored_entries=("$ignored_home/.local/state/mise/ignored-configs/"*)
|
||||
[[ -L ${ignored_entries[0]} ]] || fail "migration preserves the explicit Mise ignore marker"
|
||||
(( ${#ignored_entries[@]} == 1 )) || fail "migration preserves exactly one Mise ignore marker"
|
||||
ignored_target=$(readlink "${ignored_entries[0]}")
|
||||
[[ $ignored_target == $ignored_home/Work || $ignored_target == $ignored_config ]] || fail "preserved Mise ignore marker still targets the Work config"
|
||||
grep -F "remains ignored by Mise" <<<"$ignored_output" >/dev/null || fail "migration reports that the custom config remains ignored"
|
||||
if mise_path_active "$ignored_home" "$ignored_project"; then
|
||||
fail "ignored config becomes active after migration"
|
||||
fi
|
||||
pass "migration preserves an explicit decision to ignore the Work config"
|
||||
|
||||
symlink_home="$test_dir/symlink-home"
|
||||
symlink_config="$symlink_home/Work/.mise.toml"
|
||||
symlink_target="$test_dir/dotfiles-mise.toml"
|
||||
mkdir -p "$(dirname "$symlink_config")"
|
||||
cat >"$symlink_target" <<'TOML'
|
||||
[env]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
KEEP = "yes"
|
||||
TOML
|
||||
ln -s "$symlink_target" "$symlink_config"
|
||||
|
||||
run_migration "$symlink_home" >/dev/null
|
||||
[[ -L $symlink_config ]] || fail "migration preserves a dotfile symlink"
|
||||
grep -F '{{ cwd }}/bin' "$symlink_target" >/dev/null && fail "symlink target retains the unsafe path"
|
||||
grep -Fx 'KEEP = "yes"' "$symlink_target" >/dev/null || fail "symlink target keeps unrelated settings"
|
||||
pass "custom dotfile symlinks survive the repair"
|
||||
Reference in new issue
Block a user