Run argv click actions through a login shell as positional params

Quickshell.execDetached(argv) ran the click target with only the shell
process's stripped environment, so GUI actions like the screenshot editor
(tensaku-edit) — resolved on the login-shell PATH the old `bash -lc` string
exec provided — stopped launching on click.

Run the argv through `bash -lc 'exec "$@"'` instead: the script text is a
constant and the arguments are passed as positional parameters, which bash
expands without re-tokenizing or re-evaluating, so injection safety is intact
while PATH and session env match the old behavior exactly.
This commit is contained in:
Ryan Hughes
2026-08-23 12:25:48 -04:00
parent 07443f3970
commit d2fd2e11c6
4 changed files with 27 additions and 16 deletions
+10 -5
View File
@@ -54,12 +54,17 @@ QtObject {
Quickshell.execDetached(["bash", "-lc", command])
}
// Run an argv vector directly, without a shell. Nothing in the array is
// reparsed, so an argument carrying attacker-controlled data (a filename, a
// title) can never turn into a command. Prefer this over execDetached for any
// command assembled from untrusted input.
// Run an argv vector safely: the script text is the constant `exec "$@"`, so
// the arguments only ever land in bash's positional parameters, which it
// expands without re-tokenizing or re-evaluating — a value carrying
// attacker-controlled data ($(id), a filename, a title) stays one literal
// argument and can never turn into a command. The login shell (-l) is what
// makes this a drop-in for execDetached: click actions launch GUI apps
// (tensaku, mpv, xdg-open) that need the same PATH and session environment the
// login shell set up. Prefer this over execDetached for any command assembled
// from untrusted input.
function execArgv(argv) {
Quickshell.execDetached(argv)
Quickshell.execDetached(["bash", "-lc", 'exec "$@"', "bash"].concat(argv))
}
function isPlainObject(value) {
@@ -75,7 +75,8 @@ function execFromHints(hints) {
// The click action as an argv vector, sent by omarchy-notification-send
// --exec-arg and carried as a JSON array string in the omarchy-exec-argv hint.
// The shell runs it with Quickshell.execDetached (no shell), so a value that an
// The shell runs it via Util.execArgv, which passes the arguments as bash
// positional parameters rather than interpolating them, so a value that an
// attacker controls — a video title, a filename, a URL — is only ever one
// argument and can never be reparsed as a command. This is the parameterized
// form: the "prepared statement" to execFromHints's string concatenation.
+5 -4
View File
@@ -361,10 +361,11 @@ Item {
if (index < 0 || index >= popupModel.count) return
var entry = popupModel.get(index)
// Preferred path: an argv vector run without a shell, so data an attacker
// controls (a video title, a filename) is only ever an argument and can
// never be reparsed as a command. Detached so it outlives the shell, which
// the installer toasts depend on: they restart the shell as their first act.
// Preferred path: an argv vector whose arguments are passed as bash
// positional parameters (never interpolated into a command), so data an
// attacker controls (a video title, a filename) is only ever an argument and
// can never be reparsed as a command. Detached so it outlives the shell,
// which the installer toasts depend on: they restart it as their first act.
var argv = NotificationLogic.parseExecArgv(entry ? entry.execArgv : "")
if (argv) {
Util.execArgv(argv)