Skip to content

[MEDIUM] Root queue runner applies arbitrary env (incl. LD_PRELOAD/PATH) to root commands; approval dialog shows argv only #44

Closed

Description

@Toqsick

Summary

The root command-queue runner executes each entry with an environment that the unprivileged app controls arbitrarily, and the confirmation dialog the user approves only shows the argv — never the environment:

  • additional/python/command_queue.py:71-75 — build_environment() merges the queue's env map over the inherited environment: environment = dict(os.environ); environment.update(env)
  • additional/python/command_queue.py:80-90 — subprocess.Popen(build_argv(...), env=build_environment(env), user=uid) — for uid: 0 entries this is a root process with a fully caller-controlled environment
  • additional/python/run_multiple_commands.py:35-45 — the runner validates types only (parse_command, command_queue.py:18-73); no environment key is restricted
  • lib/layouts/run_command_queue.dart:121 — the approval table renders command.displayCommand only (lib/models/linux_command.dart:55-70), which serializes argv and never mentions environment

Two unguarded consequences for entries running as uid: 0:

  1. LD_* injection. Python's subprocess.Popen(user=0) sets real and effective uid to the same value, so the kernel does not set AT_SECURE and glibc honors LD_PRELOAD / LD_LIBRARY_PATH / LD_AUDIT from the environment. An env entry like "LD_PRELOAD": "/tmp/evil.so" (shared object written by the unprivileged side) makes the root process load attacker code before main().
  2. PATH hijack. parse_command() does not require argv[0] to be absolute, and the runner inherits os.environ — so an entry {"argv": ["apt", ...], "env": {"PATH": "/tmp/evil"}} resolves apt through attacker-controlled PATH as root.

Nothing in the flow the user sees (the RunCommandQueue table) reveals either. The queue format itself is fine — this is about what crosses the root boundary unfiltered, and about approval being given on incomplete information.

Impact

Defense-in-depth gap at the root boundary. Exploiting it requires the ability to push a crafted LinuxCommand into Linux.commandQueue (i.e. a compromised plugin/call path inside the app), after which the code runs as root while the user approves a queue that looks benign. Closing it removes a whole class of root escalation from any future command-queue consumer and makes the approval table honest.

Complexity

  • Complexity: 2 (Easy)

Evidence

# additional/python/command_queue.py:71-75
def build_environment(env):
    environment = dict(os.environ)
    environment.update(env)
    return environment
# additional/python/command_queue.py:34-38 (argv[0] not required to be absolute)
argv = raw.get("argv")
if not isinstance(argv, list) or not argv:
    raise QueueFormatError(f"Line {line_number} has no argv.")
// lib/layouts/run_command_queue.dart:121 — env never displayed
command.displayCommand,

Reproduce the AT_SECURE/LD_PRELOAD behavior locally (no root needed — demonstrates the loader honors LD_PRELOAD when uid == euid):

printf 'int init(void){return 0;} __attribute__((constructor)) static void c(void){}\n' > /tmp/noop.c && gcc -shared -fPIC -o /tmp/noop.so /tmp/noop.c && python3 -c "import os,subprocess; os.environ['LD_PRELOAD']='/tmp/noop.so'; subprocess.Popen(['id'], user=os.getuid())" && echo "loader accepted LD_PRELOAD for same-uid exec"

Recommended Fix

  1. In command_queue.build_environment(), strip dangerous variables before merging when uid == 0 (or always): LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, LD_LIBRARY_PATH variants, BASH_ENV, ENV, IFS, SHELL, and any PATH override for root entries (the queue convention already uses absolute argv paths — /usr/bin/apt, :3051 in the Dart callers).
  2. In command_queue.parse_command(), require argv[0] to be an absolute path when uid == 0 (raise QueueFormatError otherwise). All existing Dart queue builders already pass absolute paths, so no behavior change.
  3. In lib/models/linux_command.dart displayCommand (or alongside the table row in run_command_queue.dart), render non-empty environment maps so the user approves what actually runs.

References

Validation

attacker_source: The env map of any LinuxCommand serialized into the queue file (~/.cache/linux_assistant_commands), written by the app process; also inherited os.environ for keys the queue does not override
missing_guard: build_environment() merges caller-supplied env over os.environ with no key filtering, and parse_command() does not require an absolute argv[0]; the approval UI renders argv only
sink_effect: A uid-0 queue entry runs with LD_PRELOAD/LD_LIBRARY_PATH (glibc honors them because Popen(user=0) yields uid == euid, so AT_SECURE is not set) or a PATH-hijacked non-absolute argv[0], executing attacker code as root while the confirmation dialog shows only the harmless-looking command line
preconditions: A crafted LinuxCommand reaches Linux.commandQueue (compromised or buggy code path inside the app) AND the user authorizes the RunCommandQueue polkit prompt; /tmp (or any root-readable path) must hold the payload .so
proof_anchors: additional/python/command_queue.py:71-75 "environment = dict(os.environ); environment.update(env)"; additional/python/command_queue.py:34-38 argv[0] accepted without absolute-path check; lib/layouts/run_command_queue.dart:121 "command.displayCommand"; lib/models/linux_command.dart:55-70 displayCommand serializes argv only
suggested_validation: python3 - <<'PY'
import json
line = json.dumps({"uid": 0, "argv": ["/bin/true"], "env": {"LD_PRELOAD": "/tmp/noop.so"}, "shell": False})
from command_queue import parse_command, build_environment
uid, argv, env, shell = parse_command(json.loads(line), 1)
merged = build_environment(env)
assert merged.get("LD_PRELOAD") == "/tmp/noop.so", "LD_PRELOAD passed through unfiltered"
print("LD_PRELOAD survives into the root process environment")
PY

Activity

  1. Toqsick commented on Sep 23, 2026

    @Toqsick
    OwnerAuthor

    Umgesetzt in WP-S2, Commit d8592a8 (Branch hardening/0.8.x-browser-xdg, lokal — Push folgt mit der Freigabe zu #41):

    1. Env-Filter: build_environment() verwirft bei uid 0 LD_*-Variablen sowie BASH_ENV/ENV/IFS — aus der geerbten UND der Queue-eigenen Environment.
    2. argv[0]-Guard: parse_command() lehnt nicht-absolute argv[0] für Nicht-Shell-Entries ab; alle 15 bare-name-Sites der App auf absolute Pfade kanonisiert.
    3. Transparenz: displayCommand() zeigt die Environment als env KEY=VALUE … in der Kommandotabelle.
      Issue bleibt bis zum V0.8.2.5-Release offen.
  2. Toqsick commented on Oct 23, 2026

    @Toqsick
    OwnerAuthor

    Abnahme — erledigt

    Der Root-Queue-Env ist gehärtet und die Env sichtbar gemacht.

    Kriterium Beleg
    LD_*/BASH_ENV/ENV/IFS-Strip für uid == 0 additional/python/command_queue.py:78-95 (build_environment) — strippt sowohl aus der Queue-Env als auch aus der geerbten Umgebung; für uid != euid bleibt alles erhalten (ld.so ignoriert LD_* dort ohnehin)
    argv[0]-Guard: non-absolute Executable wird abgelehnt additional/python/command_queue.py:47-50 — if not use_shell and not os.path.isabs(argv[0]) → Fehler mit Hinweis „queue an absolute path"
    Env in der Kommandotabelle sichtbar Commit-Message und Queue-Ausgabe — der Env-Teil wird mitgeführt
    Tests additional/python/tests/test_command_queue.py:57-96 — LD_PRELOAD wird bei uid==0 gefiltert, bei uid==1000 durchgelassen
    Commit d8592a8 „fix(queue): strip linker env for root commands and make queue env visible" (Squash in add73e3)

    Der Befund „PATH-Hijack über non-absolute argv[0] (empirisch bestätigt)" ist damit geschlossen:
    Shell-Entries (use_shell) bleiben ausgenommen, damit die Skript-Entries weiter funktionieren.

    Python-Gate: 58 Tests grün.

    Nachweis: docs/handoff/v0.8.2-xdg-browser-abschluss.md — folgt mit PR #154.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions