Skip to content

[MEDIUM] Search auto-executes executable files from recent-files/favorites/folder scan without confirmation (openfile:) #49

Closed

Description

@Toqsick

Summary

The openfile: action executes a file as a program in a terminal when the file happens to be executable — without any confirmation dialog. The code's own comments admit the gap:

  • lib/services/action_handler.dart:147-160 — openfile: handler:
    bool isExecutable = await Linux.isFileExecutable(file);
    if (isExecutable) {
      // TODO: Ask the user, if the file should be run (possible security risk).
      // TODO: Ask the user, if the file should be run in a terminal.
      unawaited(Linux.runExecutableInTerminal(file));
    } else {
      unawaited(Linux.runCommandWithCustomArguments("xdg-open", [file]));
    }
  • lib/services/linux.dart:2634-2643 — runExecutableInTerminal hands the path to gnome-terminal -- <path> / term -e <path> / xterm -e <path>.

openfile: entries are built from three untrusted sources and injected into the search results automatically:

  • lib/services/linux.dart:1046-1060 (getRecentFiles) — every URI in the GTK RecentManager list (additional/python/get_recent_files.py, ~/.local/share/recently-used.xbel) becomes an openfile: entry with priority -15.
  • lib/services/linux.dart:1716-1727 (getFavoriteFiles) — xapp favorites (additional/python/get_favorite_files.py).
  • lib/services/linux.dart:921-1005 (getAllFolderEntriesOfUser) — a folder scan of the user's home (find up to folder_recursion_depth).

The recent-files source is the dangerous one: recently-used.xbel is per-user state that any application running as the user can write (including sandboxed apps with home access), so entries in the search list are third-party data, not app-owned. The search also matches by filename, so an entry surfaces for whatever the user types.

This is distinct from the quoting defect tracked in #42: even with runInShell: false everywhere, the file is still executed — the sink here is "untrusted path becomes a running program", not "untrusted text becomes shell code".

Impact

A file planted by another app (or a downloaded file with a misleading name — e.g. an executable named invoice_2026.pdf or Anleitung.txt) is offered as "Open " in the main search. One click runs it under the invoking user in a terminal window with no confirmation dialog. An attacker who can write into a directory the user scans (Downloads, a synced cloud folder, a mounted USB stick) or into recently-used.xbel gets a one-click code-execution vector that rides on the user trusting Linux Assistant's search results.

Complexity

  • Complexity: 2 (Easy)

Evidence

Repro path:

  1. As another local app (or by hand): register an executable file in the GTK recent list — e.g. gio recent:/// semantics or simply open any file once with an editor after chmod +x:
    echo -e '#!/bin/sh\ntouch /tmp/pwned' > "$HOME/Downloads/Anleitung.txt"
    chmod +x "$HOME/Downloads/Anleitung.txt"
    gio open "$HOME/Downloads/Anleitung.txt"   # adds a recently-used.xbel entry
  2. Start Linux Assistant, search for Anleitung — the entry "Open Anleitung.txt" appears (lib/services/linux.dart:1046-1060).
  3. Click it → isExecutable is true → runExecutableInTerminal runs #!/bin/sh touch /tmp/pwned (lib/services/action_handler.dart:157-158, lib/services/linux.dart:2634-2643).
# confirms the code path exists without any confirmation gate:
grep -n "Ask the user, if the file should be run" -A 8 lib/services/action_handler.dart

Recommended Fix

In ActionHandler.handleActionEntry (lib/services/action_handler.dart:147-160):

  1. Show a confirmation dialog before executing an entry whose target isExecutable — mirror the pattern already used by clean_timeshift.dart's _confirmDeletion (MintY dialog, await the answer, bail on false).
  2. Make the dialog name what will happen ("This file is executable — run it in a terminal?"), and keep the plain xdg-open branch (non-executables) unconfirmed, as today.
  3. Optionally mark executable entries in the search list (icon/badge) so the user sees the risk before clicking.

Related: #42 (same call sites, quoting defect — fix both, but note the confirmation is still required after #42 lands).

References

Validation

  • attacker_source: third-party file names and paths — GTK RecentManager (recently-used.xbel, writable by any app as the user), xapp favorites, and the home folder scan (find up to folder_recursion_depth)
  • missing_guard: lib/services/action_handler.dart:147-160 checks only isExecutable and then executes; no confirmation dialog, no content/type check, no "was this file opened as a document?" distinction (the two TODO comments mark the missing guard)
  • sink_effect: Linux.runExecutableInTerminal(file) (lib/services/linux.dart:2634-2643) launches gnome-terminal -- <path> / xterm -e <path> — the untrusted file runs as a program under the invoking user
  • preconditions: the attacker-controlled file exists on disk and carries the executable bit (directly or via a planted recently-used.xbel entry), and the user clicks its search entry
  • proof_anchors: lib/services/action_handler.dart:147-160 (TODO: Ask the user, if the file should be run (possible security risk). → unawaited(Linux.runExecutableInTerminal(file));); lib/services/linux.dart:1057 (action: "openfile:$recentFile" built from additional/python/get_recent_files.py output)
  • suggested_validation: grep -n "runExecutableInTerminal(file)" -B 6 lib/services/action_handler.dart — shows the execute branch with only an isExecutable check and no confirmation; after the fix, the same grep should show a dialog result gate, and the manual repro above should show a confirmation prompt instead of running the file

Activity

  1. Toqsick commented on Sep 15, 2026

    @Toqsick
    OwnerAuthor

    Fixed in 0024fcd (local, not pushed)

    lib/services/action_handler.dart, openfile: branch — a confirmation dialog now gates the exec:

    • the isExecutable branch asks first, and the question contains the full path (runFileQuestion(path) + runFileWarning); the terminal window that opens next only shows which file is running once it already is.
    • declining returns before the callback → the search list stays where it was, nothing is started.
    • the non-executable branch still goes straight to xdg-open, byte-identical to before.
    • both TODO comments removed; context.mounted checked across the first await gap in that branch.

    Verification

    • New test/action_handler_test.dart (3 widget tests): confirmed → runner called exactly once with the path, callback fires; cancelled → neither the runner nor xdg-open is called and the list stays; non-executable → no dialog, xdg-open called.
    • The three process calls in that branch are injectable via ActionHandler.debugOverride, with independent override semantics so a partial override cannot re-arm a real gnome-terminal during flutter test.
    • l10n: runFileQuestion / runFileWarning added in all four locales the ratchet in test/l10n_test.dart checks (en/de/it/fi); budgets unchanged. app_it.arb additionally got the previously missing executeInTerminal.
    • Gates: check-versions ✓ · dart format ✓ · flutter analyze 0 findings ✓ · flutter test +181 ✓ · python unittest 42 ✓
    • Manual gate passed on the release bundle: search hit openfile:/tmp/la-wps1-probe/la-wps1-probe.sh → dialog with the full path → Cancel ran nothing (side-effect file unchanged, no terminal window, list stayed) → Confirm ran the file. Reproduced twice. Evidence in the run dir under final/wp-s1-2026-09-15/.

    Not included

    The optional executable badge in the search result card. It needs a new ActionEntry field or a per-card async stat plus list state — a feature, not this fix. Noted as a follow-up.

    Note on the manual probe

    flutter build linux --release alone produces a bundle without additional/; in release Linux.getExecutableFolder() resolves the bundle path, so get_environment.py fails and the app dies in Linux.getCurrentEnvironment with RangeError (length) … before runApp — a blank window. Use bash build-bundle.sh. This is a packaging artifact, not an app bug.

    Issue stays open — release/tag pending. #42 and #56 are untouched here.

  2. Toqsick commented on Oct 23, 2026

    @Toqsick
    OwnerAuthor

    Abnahme — erledigt

    Vor dem Ausführen einer ausführbaren Datei aus dem Recent-/Favorites-Index gibt es jetzt einen
    Bestätigungs-Dialog.

    Kriterium Beleg
    Bestätigungs-Dialog lib/services/action_handler.dart:70-90 (_confirmExecution, AlertDialog mit l10n-Text runFileQuestion / runFileWarning)
    Aufrufstelle im openfile:-Zweig lib/services/action_handler.dart:214-228 — await _confirmExecution(context, file), bei Ablehnung return vor dem unawaited(_terminalRunner(file)); context.mounted-Check davor
    Kein Dialog bei Nicht-Executables lib/services/action_handler.dart:225 — der xdg-open-Pfad bleibt dialogfrei
    openfolder:-Zweig unverändert lib/services/action_handler.dart:208-212 — kein Exec-Vektor
    Widget-Tests test/action_handler_test.dart:100+ — „asks first, and starts the file only once confirmed"; executableChecker und _terminalRunner sind injizierbar (:69)
    Commit 0024fcd „fix(search): confirm before executing files from recent/favorites"

    Der isFileExecutable-Mode-Bit-Check bleibt, aber er ist jetzt die Vorbedingung für den
    Dialog statt der Freigabe zum Ausführen.

    Gates am 2026-10-23 (frisch, echte Exitcodes): flutter analyze → No issues found! ·
    flutter test → 223 Tests grün · dart format --set-exit-if-changed lib test → 123 Dateien, 0 geändert ·
    Python-Unittests 58 + 96 grün · packages/la_core 0 Findings / 25 Dateien / 68 Tests.

    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