Files
omarchycn/test/shell.d/tailscale-receive-test.sh
T
Ryan Hughes 07443f3970 Run notification click actions as argv, not shell strings
The click action of a notification was a free-form shell string run through
`bash -lc`, safe only when every sender shell-quoted every interpolated value
perfectly. One slip is RCE: a hostile yt-dlp video title forged an output
record and injected an mpv option into the click command (mehmetince.net RCE,
partially addressed by #7847).

Add a parameterized transport: omarchy-notification-send gains --exec-arg
(repeatable), encoding a JSON argv into the omarchy-exec-argv hint. The shell
runs it with Quickshell.execDetached(argv) and no shell, so data an attacker
controls is only ever one argument and can never be reparsed as a command. The
shell fails closed on a malformed argv hint.

The legacy free-form --exec string is retained but honored only from Omarchy's
own omarchy-action toasts, and deprecated. Migrate all in-repo callers
(screenshot, screen recording, taildrop receive, migrate-notify, crash-watch,
yt-dlp host) to --exec-arg. Update docs and tests.
2026-08-23 12:00:03 -04:00

96 lines
3.7 KiB
Bash

#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
WORKDIR=$(mktemp -d)
cleanup() { rm -rf "$WORKDIR"; }
trap cleanup EXIT
downloads="$WORKDIR/downloads"
mkdir -p "$WORKDIR/bin" "$downloads" "$WORKDIR/outbox"
printf 'mine' >"$downloads/unrelated.txt"
# Stands in for the daemon handing over whatever is waiting in the inbox. A
# decoy is whatever else drops into the downloads directory while Taildrop is
# still blocking on the next delivery.
cat >"$WORKDIR/bin/tailscale" <<SH
#!/bin/bash
target="\${*: -1}"
[[ -n \${DECOY:-} ]] && printf 'iso' >"$downloads/\$DECOY"
mv "$WORKDIR/outbox/"* "\$target/"
SH
cat >"$WORKDIR/bin/omarchy-notification-send" <<SH
#!/bin/bash
printf '%s\n' "\$*" >>"$WORKDIR/notifications"
SH
chmod +x "$WORKDIR/bin/"*
receive() {
local expected="$1"
shift
: >"$WORKDIR/notifications"
PATH="$WORKDIR/bin:$PATH" "$@" "$ROOT/bin/omarchy-tailscale-receive" --once "$downloads"
for _ in {1..50}; do
(($(wc -l <"$WORKDIR/notifications") >= expected)) && break
sleep 0.1
done
}
printf 'png' >"$WORKDIR/outbox/photo.png"
printf 'pdf' >"$WORKDIR/outbox/notes with space.pdf"
receive 2 env
notifications=$(<"$WORKDIR/notifications")
[[ -f $downloads/photo.png && -f "$downloads/notes with space.pdf" ]] ||
fail "taildrop receive saves incoming files" "$(ls "$downloads")"
pass "taildrop receive saves incoming files"
grep -qF -- "Received photo.png Saved to $downloads --image $downloads/photo.png" <<<"$notifications" ||
fail "taildrop receive previews received images" "$notifications"
pass "taildrop receive previews received images"
grep -q "^Received notes with space.pdf .* -g " <<<"$notifications" ||
fail "taildrop receive announces other files with a glyph" "$notifications"
pass "taildrop receive announces other files with a glyph"
# The shell keeps the click command with the toast, so receiving does not have
# to sit blocked on an answer -- and the toast still opens the file after a shell
# restart. The path rides as its own --exec-arg, so the shell runs it as literal
# data with no quoting for a name with spaces to get wrong.
grep -qF -- "--exec-arg xdg-open --exec-arg $downloads/photo.png" <<<"$notifications" ||
fail "taildrop receive attaches the open command to the notification" "$notifications"
grep -qF -- "--exec-arg xdg-open --exec-arg $downloads/notes with space.pdf" <<<"$notifications" ||
fail "taildrop receive carries spaced names as a literal open argument" "$notifications"
pass "taildrop receive lets a click open the received file"
grep -q "unrelated.txt" <<<"$notifications" &&
fail "taildrop receive leaves the rest of the downloads directory alone" "$notifications"
pass "taildrop receive leaves the rest of the downloads directory alone"
# A second delivery of the same name, alongside a download that arrives while
# Taildrop is waiting.
printf 'png' >"$WORKDIR/outbox/photo.png"
receive 1 env DECOY=browser-download.iso
notifications=$(<"$WORKDIR/notifications")
[[ -f $downloads/photo-1.png ]] || fail "taildrop receive keeps both files on a name clash" "$(ls "$downloads")"
grep -q "^Received photo-1.png " <<<"$notifications" ||
fail "taildrop receive keeps both files on a name clash" "$notifications"
pass "taildrop receive keeps both files on a name clash"
grep -q "browser-download.iso" <<<"$notifications" &&
fail "taildrop receive ignores downloads that arrive while it waits" "$notifications"
pass "taildrop receive ignores downloads that arrive while it waits"
[[ -z $(ls -A "$downloads/.omarchy-taildrop") ]] ||
fail "taildrop receive empties its staging directory" "$(ls -A "$downloads/.omarchy-taildrop")"
pass "taildrop receive empties its staging directory"