Keep relayed text out of notify-send's option parser

The argv click command closed injection through the hint's value, but the
sender still handed the headline and description to notify-send bare. A
value beginning with a dash is parsed there as flags, and one shaped like
`--hint=string:omarchy-exec-argv:[...]` is read as a hint of its own --
libnotify keys hints in a hash table, so the later of two replaces the
earlier and a forged headline outranks the vector --exec built.

That is reachable without any --exec in sight: omarchy-tailscale-send
passes a single file's basename verbatim as the description, so a file
named like the hint gives its click action to whoever chose the name.

Put the headline and description behind a `--` so notify-send reads them
as text, and refuse any pass-through word carrying omarchy-exec-argv --
--exec is the only thing that may build a click command.

Co-Authored-By: Codex XHigh <noreply@openai.com>
This commit is contained in:
Claude Opus 5 (1M context)
2026-08-23 21:44:06 +02:00
co-authored by Codex XHigh
parent 21cbbf8194
commit 1b15120d27
3 changed files with 42 additions and 4 deletions
+13 -2
View File
@@ -97,6 +97,14 @@ while (($# > 0)); do
elif parse_omarchy_option "$@"; then
shift "$parsed_option_args"
else
# --exec is the only door to a click command. A relayed title or filename
# that lands here -- passthrough is the one position an untrusted value can
# still reach notify-send as an option -- must not be able to set the hint
# itself, which is the injection this transport exists to close.
if [[ $1 == *omarchy-exec-argv* ]]; then
echo "The click command hint can only be set with --exec, not passed through." >&2
exit 1
fi
args+=("$1")
shift
fi
@@ -136,8 +144,11 @@ if ((exec_present)); then
args+=("--hint=string:omarchy-exec-argv:$exec_argv_json")
fi
# `--` so notify-send reads the headline and description as text. Without it a
# headline beginning with a dash is parsed as options ("-rf x" becomes -r), and
# one shaped like `--hint=string:...` sets a hint of its own.
if [[ -n $description ]]; then
notify-send "${args[@]}" "$headline" "$description"
notify-send "${args[@]}" -- "$headline" "$description"
else
notify-send "${args[@]}" "$headline"
notify-send "${args[@]}" -- "$headline"
fi
+6
View File
@@ -127,6 +127,12 @@ and needs no notification to execute code. What is fully closed is untrusted
*content* — web notifications can't set the exec hint at all, and any relayed
title/filename is confined to inert argument data.
The sender keeps that last part true rather than leaving it to each caller. The
headline and description go to `notify-send` behind a `--`, so a relayed value
beginning with a dash is text and not flags, and a word that reaches the
pass-through option position carrying `omarchy-exec-argv` is refused outright:
`--exec` is the only thing that may build a click command.
## Helper commands
- `omarchy-notification-wait [timeout]` — polls until the shell answers IPC
+23 -2
View File
@@ -34,8 +34,9 @@ mapfile -t args <"$args_file"
[[ ${args[4]} == "--hint=string:omarchy-glyph:K" ]] || fail "notification wrapper converts glyph to hint"
[[ ${args[5]} == "--hint=string:image-path:/tmp/image.png" ]] || fail "notification wrapper converts image to hint"
[[ ${args[6]} == '--hint=string:omarchy-exec-argv:["omarchy-menu-keybindings","a b"]' ]] || fail "notification wrapper converts the click command to an argv hint" "${args[6]}"
[[ ${args[7]} == "Learn Keybindings" ]] || fail "notification wrapper preserves headline"
[[ ${args[8]} == "Body" ]] || fail "notification wrapper preserves description"
[[ ${args[7]} == "--" ]] || fail "notification wrapper ends the options before the text" "${args[7]}"
[[ ${args[8]} == "Learn Keybindings" ]] || fail "notification wrapper preserves headline"
[[ ${args[9]} == "Body" ]] || fail "notification wrapper preserves description"
pass "notification wrapper supports app, glyph, urgency, image, and exec options"
# The shell runs the click command itself, so nothing may block the sender on a
@@ -97,3 +98,23 @@ argv_json=${argv_hint#--hint=string:omarchy-exec-argv:}
grep -qx -- "--exec" "$args_file" || fail "notification wrapper keeps a --exec-looking headline as text"
grep -q 'image-path:/tmp/i.png' "$args_file" || fail "notification wrapper still parses options after a --exec-looking headline"
pass "notification wrapper does not treat a --exec-looking positional as the delimiter"
# The headline and description are text, never options. notify-send parses a
# leading-dash summary as flags ("-rf x" is -r with the value x) and reads a
# `--hint=string:...` word as a hint, so they go behind a `--` separator.
: >"$args_file"
send "-rf oops" "a body" >/dev/null
mapfile -t args <"$args_file"
[[ ${args[-3]} == "--" ]] || fail "notification wrapper separates a dash headline from the options" "${args[*]}"
[[ ${args[-2]} == "-rf oops" ]] || fail "notification wrapper keeps a dash headline as text" "${args[*]}"
[[ ${args[-1]} == "a body" ]] || fail "notification wrapper keeps the description after a dash headline" "${args[*]}"
pass "notification wrapper hands the headline to notify-send as text, not options"
# The click command has exactly one door. A relayed title or filename that
# reaches option position must not be able to forge the hint --exec produces.
: >"$args_file"
if send "Download complete" '--hint=string:omarchy-exec-argv:["sh","-c","touch /tmp/pwned"]' 2>/dev/null; then
fail "notification wrapper rejects a forged click-command hint"
fi
grep -q "omarchy-exec-argv" "$args_file" && fail "notification wrapper sends nothing when a click hint is forged"
pass "notification wrapper refuses a click-command hint it did not build from --exec"