Refuse a theme name that is shell syntax, and quote the one the unlock picker returns
A theme installed from a git repo is named after the repo URL, and that name becomes its directory name under ~/.config/omarchy/themes. Style > Unlock built a command line out of the name the picker returned and handed it to omarchy-launch-floating-terminal-with-presentation, which runs its argument as a shell string -- so a theme directory called `a';id;'b` ran `id`. Themes are already held to contributing colour and nothing that executes, which is why omarchy-theme-set stages no .lua, terminal config, or vscode.json from one. Hold the derived name to the characters a theme name needs, which stops it from being dangerous at every place it lands rather than at the one found, and quote it with printf %q on the way into the action for the names already on disk. omarchy-theme-remove keeps its existing path-climb guard: its name reaches only a quoted rm, and the same charset would strand a theme installed before this. Reported-by: Luis Alvarez (lalvarezt) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011WFcUm5HWFyxaVYdwAeWPP
This commit is contained in:
co-authored by
Claude Opus 5
parent
4637735aa2
commit
75e51f0b95
@@ -29,10 +29,15 @@ REPO_PATH="$REPO_URL"
|
||||
THEME_NAME=$(basename -- "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]')
|
||||
THEME_PATH="$THEMES_DIR/$THEME_NAME"
|
||||
|
||||
# The name comes from the URL and is joined into a path that is about to be
|
||||
# removed, so a repo called `..` would take ~/.config/omarchy with it. A leading
|
||||
# dot is refused with it: `host:-s/foo.git` leaves basename with `.git`.
|
||||
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then
|
||||
# The name comes from the URL, is joined into a path that is about to be
|
||||
# removed, and then names a directory the rest of Omarchy passes around by
|
||||
# name: Style > Unlock builds a command line out of the one the picker
|
||||
# returned. So it is held to the characters a theme name needs rather than
|
||||
# screened for the harm of the day -- a repo called `..` would take
|
||||
# ~/.config/omarchy with it, and one called `a';'id` would carry its own
|
||||
# command into that picker. The leading character is kept out of `.` and `-`,
|
||||
# which also covers `host:-s/foo.git` leaving basename with `.git`.
|
||||
if [[ ! $THEME_NAME =~ ^[a-z0-9][a-z0-9._-]*$ ]]; then
|
||||
echo "Error: '$REPO_URL' does not give a usable theme name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
// Style
|
||||
"style.theme": {"icon":"","label":"Theme","aliases":["theme","themes"],"action":"theme=$(omarchy-theme-switcher); [[ -n $theme ]] && omarchy-theme-set \"$theme\""},
|
||||
"style.background": {"icon":"","label":"Background","aliases":["background","wallpaper"],"action":"background=$(omarchy-theme-bg-switcher); [[ -n $background ]] && omarchy-theme-bg-set \"$background\""},
|
||||
"style.unlock": {"icon":"","label":"Unlock","aliases":["unlock"],"action":"unlock=$(omarchy-plymouth-switcher); if [[ $unlock == default ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-plymouth-reset; elif [[ -n $unlock ]]; then omarchy-launch-floating-terminal-with-presentation \"omarchy-plymouth-set-by-theme '$unlock'\"; fi"},
|
||||
"style.unlock": {"icon":"","label":"Unlock","aliases":["unlock"],"action":"unlock=$(omarchy-plymouth-switcher); if [[ $unlock == default ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-plymouth-reset; elif [[ -n $unlock ]]; then omarchy-launch-floating-terminal-with-presentation \"omarchy-plymouth-set-by-theme $(printf %q \"$unlock\")\"; fi"},
|
||||
"style.font": {"icon":"","label":"Font","provider":"fonts"},
|
||||
"style.bar": {"icon":"","label":"Menu Bar"},
|
||||
"style.bar.position": {"icon":"","label":"Position"},
|
||||
|
||||
@@ -41,3 +41,103 @@ grep -Fq 'sudo cp "$staging_dir/logo.png" "$sddm_dir/logo.png"' "$ROOT/bin/omarc
|
||||
fail "omarchy-plymouth-set copies the staged logo to SDDM rather than rereading the caller's path as root"
|
||||
|
||||
pass "a themed logo cannot republish a file it merely points at"
|
||||
|
||||
# Style > Unlock picks a theme by name and hands the answer to
|
||||
# omarchy-launch-floating-terminal-with-presentation, which joins its arguments
|
||||
# into a script and runs that with `bash -c`. So the name is shell source
|
||||
# unless the action quotes it -- and the name is a directory name under
|
||||
# ~/.config/omarchy/themes, which a theme installed from a git repo gets from
|
||||
# the repo URL. `a';id;'b` is a legal directory name.
|
||||
require_command node
|
||||
|
||||
unlock_action=$(node -e '
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const menu = require(path.join(process.env.ROOT, "shell/plugins/menu/MenuModel.js"))
|
||||
const items = menu.parseMenuJsonc(fs.readFileSync(path.join(process.env.ROOT, "default/omarchy/omarchy-menu.jsonc"), "utf8"))
|
||||
process.stdout.write(items.find(item => item.id === "style.unlock").action)
|
||||
')
|
||||
|
||||
[[ -n $unlock_action ]] || fail "the shipped menu still carries a style.unlock action"
|
||||
|
||||
stub_dir="$test_tmp/stubs"
|
||||
mkdir -p "$stub_dir"
|
||||
|
||||
canary="$test_tmp/canary"
|
||||
set_args="$test_tmp/set-args"
|
||||
reset_marker="$test_tmp/reset-ran"
|
||||
|
||||
# What a name that got reparsed would reach. It is a command rather than a
|
||||
# `touch` so that no quoting of the test's own paths is involved.
|
||||
cat >"$stub_dir/omarchy-test-canary" <<STUB
|
||||
#!/bin/bash
|
||||
printf 'ran\n' >"$canary"
|
||||
STUB
|
||||
|
||||
cat >"$stub_dir/omarchy-plymouth-switcher" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$OMARCHY_TEST_UNLOCK_NAME"
|
||||
STUB
|
||||
|
||||
# Stands in for the real wrapper, which is a shell-string API: it interpolates
|
||||
# "$*" into a script and hands that to `bash -c`. The grep below is what keeps
|
||||
# this stub honest if the wrapper ever stops working that way.
|
||||
cat >"$stub_dir/omarchy-launch-floating-terminal-with-presentation" <<'STUB'
|
||||
#!/bin/bash
|
||||
exec bash -c "omarchy-show-logo; $*; omarchy-show-done"
|
||||
STUB
|
||||
|
||||
grep -Fq 'bash -c "$presentation_script"' "$ROOT/bin/omarchy-launch-floating-terminal-with-presentation" ||
|
||||
fail "the presentation wrapper still runs its argument as a shell string, as the stub above assumes"
|
||||
|
||||
# Records what actually arrived, so a name that survived as data is told apart
|
||||
# from one that arrived split or partly eaten.
|
||||
cat >"$stub_dir/omarchy-plymouth-set-by-theme" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$#" "$@" >"$OMARCHY_TEST_SET_ARGS"
|
||||
STUB
|
||||
|
||||
cat >"$stub_dir/omarchy-plymouth-reset" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'ran\n' >"$OMARCHY_TEST_RESET_MARKER"
|
||||
STUB
|
||||
|
||||
for command in omarchy-show-logo omarchy-show-done; do
|
||||
printf '#!/bin/bash\nexit 0\n' >"$stub_dir/$command"
|
||||
done
|
||||
|
||||
chmod +x "$stub_dir"/*
|
||||
|
||||
run_unlock_action() {
|
||||
rm -f "$canary" "$set_args" "$reset_marker"
|
||||
|
||||
PATH="$stub_dir:$PATH" \
|
||||
OMARCHY_TEST_UNLOCK_NAME="$1" \
|
||||
OMARCHY_TEST_SET_ARGS="$set_args" \
|
||||
OMARCHY_TEST_RESET_MARKER="$reset_marker" \
|
||||
bash -c "$unlock_action" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# A directory name cannot hold a slash or a NUL, and everything else is fair
|
||||
# game -- these are the shapes that would run on the way to the picker.
|
||||
for name in "a';omarchy-test-canary;'b" 'a$(omarchy-test-canary)b' 'a`omarchy-test-canary`b' 'a b' '-a'; do
|
||||
run_unlock_action "$name"
|
||||
|
||||
[[ ! -e $canary ]] || fail "a theme name reaches the unlock screen as data, not as shell" "ran for: $name"
|
||||
[[ $(cat "$set_args" 2>/dev/null) == $'1\n'"$name" ]] ||
|
||||
fail "the unlock screen gets the theme name whole" "$name: $(cat "$set_args" 2>/dev/null)"
|
||||
done
|
||||
|
||||
pass "a theme name cannot carry a command into the unlock screen"
|
||||
|
||||
# The two ordinary paths still work: a named theme is applied, and `default`
|
||||
# resets rather than being looked up as a theme.
|
||||
run_unlock_action "tokyo-night"
|
||||
[[ $(cat "$set_args" 2>/dev/null) == $'1\ntokyo-night' ]] ||
|
||||
fail "an ordinary theme name still reaches omarchy-plymouth-set-by-theme" "$(cat "$set_args" 2>/dev/null)"
|
||||
|
||||
run_unlock_action "default"
|
||||
[[ -e $reset_marker ]] || fail "picking default still resets the unlock screen"
|
||||
[[ ! -e $set_args ]] || fail "picking default does not look up a theme named default" "$(cat "$set_args")"
|
||||
|
||||
pass "the unlock picker still applies a theme and still resets on default"
|
||||
|
||||
@@ -92,6 +92,34 @@ done
|
||||
|
||||
pass "a URL whose name would climb out of the themes directory never reaches git"
|
||||
|
||||
# The derived name outlives the clone: it is the theme's directory name, and
|
||||
# Style > Unlock builds a command line out of the name the picker returned. A
|
||||
# repo whose name carries shell syntax would hand that picker its own command,
|
||||
# so the name is refused here rather than quoted at each place it lands.
|
||||
for url in \
|
||||
"https://example.com/omarchy-a';id;'b-theme.git" \
|
||||
'https://example.com/a$(id).git' \
|
||||
'https://example.com/a`id`.git' \
|
||||
"https://example.com/a b.git" \
|
||||
"https://example.com/-a.git"; do
|
||||
if install_theme "$url"; then
|
||||
fail "omarchy-theme-install refuses the derived name from '$url'"
|
||||
fi
|
||||
|
||||
[[ ! -s $git_calls ]] || fail "omarchy-theme-install refuses '$url' before running git" "$(cat "$git_calls")"
|
||||
done
|
||||
|
||||
pass "a URL whose name would be shell syntax never reaches git"
|
||||
|
||||
# And the check is an allowlist, so the punctuation a real theme name uses has
|
||||
# to keep working.
|
||||
install_theme "https://github.com/example/omarchy-tokyo_night.2-theme.git" ||
|
||||
fail "omarchy-theme-install accepts the punctuation a theme name uses"
|
||||
grep -Fq "/themes/tokyo_night.2" "$git_calls" ||
|
||||
fail "omarchy-theme-install derives a name carrying an underscore and a dot" "$(cat "$git_calls")"
|
||||
|
||||
pass "a theme name may still hold an underscore, a dot, and a dash"
|
||||
|
||||
# basename reads a leading dash as an option once the scp-style prefix is gone.
|
||||
install_theme "host:-s/foo.git" || fail "omarchy-theme-install accepts a normal scp-style URL"
|
||||
grep -Fq -- "-- host:-s/foo.git" "$git_calls" || fail "omarchy-theme-install passes the URL after --" "$(cat "$git_calls")"
|
||||
|
||||
Reference in New Issue
Block a user