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:
Mehmet Ince
2026-08-26 16:22:17 +01:00
co-authored by Claude Opus 5
parent 4637735aa2
commit 75e51f0b95
4 changed files with 138 additions and 5 deletions
+9 -4
View File
@@ -29,10 +29,15 @@ REPO_PATH="$REPO_URL"
THEME_NAME=$(basename -- "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]') THEME_NAME=$(basename -- "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]')
THEME_PATH="$THEMES_DIR/$THEME_NAME" THEME_PATH="$THEMES_DIR/$THEME_NAME"
# The name comes from the URL and is joined into a path that is about to be # The name comes from the URL, is joined into a path that is about to be
# removed, so a repo called `..` would take ~/.config/omarchy with it. A leading # removed, and then names a directory the rest of Omarchy passes around by
# dot is refused with it: `host:-s/foo.git` leaves basename with `.git`. # name: Style > Unlock builds a command line out of the one the picker
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then # 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." echo "Error: '$REPO_URL' does not give a usable theme name."
exit 1 exit 1
fi fi
+1 -1
View File
@@ -103,7 +103,7 @@
// Style // Style
"style.theme": {"icon":"󰸌","label":"Theme","aliases":["theme","themes"],"action":"theme=$(omarchy-theme-switcher); [[ -n $theme ]] && omarchy-theme-set \"$theme\""}, "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.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.font": {"icon":"","label":"Font","provider":"fonts"},
"style.bar": {"icon":"󰍜","label":"Menu Bar"}, "style.bar": {"icon":"󰍜","label":"Menu Bar"},
"style.bar.position": {"icon":"","label":"Position"}, "style.bar.position": {"icon":"","label":"Position"},
+100
View File
@@ -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" 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" 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"
+28
View File
@@ -92,6 +92,34 @@ done
pass "a URL whose name would climb out of the themes directory never reaches git" 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. # 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" 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")" grep -Fq -- "-- host:-s/foo.git" "$git_calls" || fail "omarchy-theme-install passes the URL after --" "$(cat "$git_calls")"