Repository navigation
[MEDIUM] Search auto-executes executable files from recent-files/favorites/folder scan without confirmation (openfile:) #49
Description
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—runExecutableInTerminalhands the path tognome-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 anopenfile: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 (findup tofolder_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:
- 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 afterchmod +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
- Start Linux Assistant, search for
Anleitung— the entry "Open Anleitung.txt" appears (lib/services/linux.dart:1046-1060). - Click it →
isExecutableis true →runExecutableInTerminalruns#!/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.dartRecommended Fix
In ActionHandler.handleActionEntry (lib/services/action_handler.dart:147-160):
- Show a confirmation dialog before executing an entry whose target
isExecutable— mirror the pattern already used byclean_timeshift.dart's_confirmDeletion(MintY dialog,awaitthe answer, bail onfalse). - Make the dialog name what will happen ("This file is executable — run it in a terminal?"), and keep the plain
xdg-openbranch (non-executables) unconfirmed, as today. - 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
- OWASP: Unrestricted File Upload / handling untrusted files — execution without user intent: https://owasp.org/www-community/vulnerabilities
- Freedesktop recently-used.xbel spec (per-user, app-writable): https://specifications.freedesktop.org/recent-file-spec/latest/
- GNOME HIG: destructive/confirm actions: https://developer.gnome.org/hig/patterns/feedback/dialogs.html
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 (findup tofolder_recursion_depth)missing_guard:lib/services/action_handler.dart:147-160checks onlyisExecutableand 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) launchesgnome-terminal -- <path>/xterm -e <path>— the untrusted file runs as a program under the invoking userpreconditions: the attacker-controlled file exists on disk and carries the executable bit (directly or via a plantedrecently-used.xbelentry), and the user clicks its search entryproof_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 fromadditional/python/get_recent_files.pyoutput)suggested_validation:grep -n "runExecutableInTerminal(file)" -B 6 lib/services/action_handler.dart— shows the execute branch with only anisExecutablecheck 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
Toqsick commentedon Sep 15, 2026
Fixed in
0024fcd(local, not pushed)lib/services/action_handler.dart,openfile:branch — a confirmation dialog now gates the exec:isExecutablebranch 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.xdg-open, byte-identical to before.context.mountedchecked across the firstawaitgap in that branch.Verification
test/action_handler_test.dart(3 widget tests): confirmed → runner called exactly once with the path, callback fires; cancelled → neither the runner norxdg-openis called and the list stays; non-executable → no dialog,xdg-opencalled.ActionHandler.debugOverride, with independent override semantics so a partial override cannot re-arm a realgnome-terminalduringflutter test.runFileQuestion/runFileWarningadded in all four locales the ratchet intest/l10n_test.dartchecks (en/de/it/fi); budgets unchanged.app_it.arbadditionally got the previously missingexecuteInTerminal.check-versions✓ ·dart format✓ ·flutter analyze0 findings ✓ ·flutter test+181 ✓ · python unittest 42 ✓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 underfinal/wp-s1-2026-09-15/.Not included
The optional executable badge in the search result card. It needs a new
ActionEntryfield 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 --releasealone produces a bundle withoutadditional/; in releaseLinux.getExecutableFolder()resolves the bundle path, soget_environment.pyfails and the app dies inLinux.getCurrentEnvironmentwithRangeError (length) …beforerunApp— a blank window. Usebash build-bundle.sh. This is a packaging artifact, not an app bug.Issue stays open — release/tag pending. #42 and #56 are untouched here.
fix(search): confirm before executing files from recent/favorites
docs(security): hand off the V0.8.2.5 security pass after WP-S1
Toqsick commentedon Oct 23, 2026
Abnahme — erledigt
Vor dem Ausführen einer ausführbaren Datei aus dem Recent-/Favorites-Index gibt es jetzt einen
Bestätigungs-Dialog.
lib/services/action_handler.dart:70-90(_confirmExecution, AlertDialog mit l10n-TextrunFileQuestion/runFileWarning)openfile:-Zweiglib/services/action_handler.dart:214-228—await _confirmExecution(context, file), bei Ablehnungreturnvor demunawaited(_terminalRunner(file));context.mounted-Check davorlib/services/action_handler.dart:225— derxdg-open-Pfad bleibt dialogfreiopenfolder:-Zweig unverändertlib/services/action_handler.dart:208-212— kein Exec-Vektortest/action_handler_test.dart:100+— „asks first, and starts the file only once confirmed";executableCheckerund_terminalRunnersind injizierbar (:69)0024fcd„fix(search): confirm before executing files from recent/favorites"Der
isFileExecutable-Mode-Bit-Check bleibt, aber er ist jetzt die Vorbedingung für denDialog 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_core0 Findings / 25 Dateien / 68 Tests.Nachweis:
docs/handoff/v0.8.2-xdg-browser-abschluss.md— folgt mit PR #154.