Fix-Plan für die 9 security/injection-Findings des RepoLens-Audit-Runs
20260912T004309Z-c22e3a62(Triage 2026-09-15:~/20-Workspace/RepoLens/logs/20260912T004309Z-c22e3a62/final/TRIAGE-2026-09-15.md). Aufbau folgtrelease-plaene-v0.8.1-v0.8.5.md: jedes Work-Paket erbt die globalen Randbedingungen — Referenzsystem Zorin OS 18.1, Branchhardening/0.8.x-browser-xdg, Security-Invarianten unangetastet (Command-Queue, zwei polkit-Actions), Commit mitCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>. Ohne Bastis OK: kein Push, kein PR, kein Versions-Bump, kein Release.Reihenfolge:
RV-1 → V0.8.1 → V0.8.2 (inkl. WP-B1/B2) → V0.8.2.5 (WP-S1/S2/S3) → V0.8.3 (inkl. WP-P1/P2) → V0.8.4 → V0.8.5
-
bash tool/check-versions.sh -
dart format --output=none --set-exit-if-changed lib test -
flutter analyze→ 0 Findings -
flutter test→ grün -
(cd additional/python && python3 -m unittest discover -s tests -t .)
| Issue | Verdict | Finale Severity | Fix-Paket |
|---|---|---|---|
| #49 | confirmed (Triage+Skeptiker) | Medium — top priority | WP-S1 (V0.8.2.5) |
| #44 | confirmed | Medium (Defense-in-Depth) | WP-S2 (V0.8.2.5) |
| #45 | contested — Mechanik-Prämisse entkräftet | Low pending RV-1 | WP-B2 (V0.8.2) |
| #50 | contested — Mechanik-Prämisse entkräftet | Low pending RV-1 | WP-S3 (V0.8.2.5) |
| #42 | rejected (Prämisse empirisch widerlegt) | Low (Härtung+Korrektheit) | WP-S3 (V0.8.2.5) |
| #47 | confirmed | Low | WP-B1 (V0.8.2) |
| #46 | partial (Interpolation durch Typ-Cast blockiert) | Low (Fragilität) | WP-S3 (V0.8.2.5) |
| #43 | confirmed (latent, Konstantpfade) | Low | WP-P1 (V0.8.3) |
| #48 | confirmed (Dead Code, 18 Root-Importe) | Low | WP-P2 (V0.8.3) |
Grundlage aller Entschärfungen zu #42/#45/#50: Dart Process.run(runInShell: true) escapet
auf POSIX jedes argv-Element per Single-Quote-Wrapping
(sdk/lib/_internal/vm/bin/process_patch.dart, unverändert seit >2019), nachgetestet
auf Dart 3.12.2 — Metazeichen (;, $(…), Backticks, *, ') sind inert, Leerzeichen
bleiben in einem Argument. RV-1 prüft das über die echte gebaute App nach.
Ziel: Der endgültige Beweis für #45/#50/#42 — die Dart-VM-Empirie der Triage lief auf
dart run (Snap), nicht auf der gebauten App. Einmaliger, günstiger Check
(flutter run -d linux), vor WP-B2 (und vor dem Filing/Update der GitHub-Issues #45/#50).
Proben:
- #45-Kette: Datei mit Namen
x'; echo INJECTED45 > /tmp/pwned45; #.txtin~/.local/share/recently-used.xbeleintragen (RecentManager); App-Search öffnen, bis die Card rendert (200 ms-Timer). Erwartet (pro Empirie): Icon-Lookup fällt auf Default, keine/tmp/pwned45. Kontrolle:don't.txt→ korrekter Icon-Lookup nach dem Fix. - #50-Kette: Mountpoint mit Leerzeichen + Metazeichen (
/mnt/test A b;c) indf -h-Output simulieren, Disk-Analyzer-Klick (openDiskSpaceAnalyzer→ linux.dart:2752/2755/2770). Erwartet: k4dirstat/baobab öffnet den Mountpoint korrekt (ein Argument), keine Injektion. - #42-Kontrolle: ein
openWebbrowserWithSite-Aufruf mit Leerzeichen im Input — prüft die String-Form-Call-Sites, die der Default-Flip in WP-S3 verändern wird.
Outcome-Gate:
- Bestätigt Empirie → #45/#50 endgültig Low (Härtung), Fix-Bündel laufen wie geplant.
- Zeigt sich doch Injektion (abweichendes Flutter-Runtime-Verhalten) → #45 zurück zu HIGH / #50 MEDIUM, WP-B2/WP-S3 werden zur Pflicht vor V0.8.3 und der Fix-Plan wird angepasst.
- Beleg (Screenshot/Log) ins
final/-Ordner des Run-Dirs neben den Triage-Report.
Ziel: Die beiden echten Security-Findings sind geschlossen und die Hygiene-Familie (Shell-Quoting/Boundary) ist konsistent gezogen — bevor V0.8.3 das Paket einfriert.
Befund: RecentManager (getRecentFiles, linux.dart:1045-1061) und xapp-Favorites
(getFavoriteFiles, linux.dart:1705-1725) erzeugen openfile:-Entries (beide Module
default-aktiv, Quelle = GTK recent-files/xbel, von jeder App schreibbar) → Substring-Match in
der Suche → Klick → action_handler.dart:147-160: isFileExecutable prüft nur Mode-Bits →
unawaited(Linux.runExecutableInTerminal(file)) → gnome-terminal -- <path> führt die Datei
aus. Die TODO-Kommentare (:160-161 „Ask the user … possible security risk") räumen die Lücke
selbst ein. (Issue-Abweichung: der Home-Scan erzeugt openfolder:-Einträge — kein Exec-Vektor.)
Fix (TDD):
- RED —
test/: Widget-Tests für den openfile-Zweig: bestätigt →runExecutableInTerminalgerufen; abgebrochen → nicht gerufen; kein Dialog bei Nicht-Executables (xdg-open-Pfad). Erwartet: Compile-Fehler, weil der Dialog noch fehlt. - GREEN — Bestätigungs-Dialog vor dem Exec-Aufruf nach existierendem Muster
lib/tools/clean_timeshift.dart:69 _confirmDeletion(await vor demunawaited,context.mounted-Check, l10n-Key für Dialogtext). - Optional: Executable-Badge im Suchergebnis-Card (sichtbarer Hinweis vor dem Klick).
Gates: CI-Gates; manuell: Suche trifft ein ausführbares Recent-File → Dialog erscheint, Abbrechen startet nichts. Commit: „fix(search): confirm before executing files from recent/favorites".
Befund: build_environment() (command_queue.py:65-73) merged Queue-env ungefiltert über
os.environ; Popen(..., user=0) ehrt LD_PRELOAD/LD_LIBRARY_PATH/LD_AUDIT bei
uid==euid; PATH-Hijack über non-absolute argv[0] (empirisch bestätigt). Kein Befehls-Approval-
Dialog existiert (nur generischer polkit-Prompt; Kommandotabelle per Config-Flag default aus,
zeigt die Queue erst während sie läuft) — Transparenz ist also eigener Wert, kein Approval-Fix.
„Alle Builder nutzen absolute Pfade" ist falsch: rm/ln/sed/timeshift/flatpak nutzen bare
argv[0], Shell-Entries tragen den Skript-String in argv[0] — ein naiver Guard bricht diese Flows.
Fix (dreischichtig, TDD):
- RED —
additional/python/tests/(nachtest_command_queue.py-Schema): Env-Map mitLD_PRELOADwird vonbuild_environment()bei uid==0 gefiltert;parse_command()lehnt non-absolute argv[0] für Nicht-Shell-Entries ab. - GREEN —
(1) LD_*/
BASH_ENV/ENV/IFS-Strip (oder Allowlist) inbuild_environment()für uid==0; (2) argv[0]-Guard nach Kanonisierung der ~8 bare-name-Sites (absolute Pfade einsetzen; Shell-Entries via Flag ausnehmen); (3) env indisplayCommand/Kommandotabelle sichtbar machen (Transparenz). - Regression: bestehende Queue-Flows (Timeshift/Flatpak/Snap-Entries) laufen unverändert.
Gates: Python-Unittests; manuell: eine Queue-Runde (Timeshift-Apply) läuft normal. Commit: „fix(queue): strip linker env for root commands and make queue env visible".
Befund #42: runInShell-Defaults sind true (linux.dart:106/:128), aber die Mechanik-Prämisse
des Issues („argv space-joined unquoted → Metazeichen werden Shell-Code") ist empirisch widerlegt
— kein Security-Bug. Real: String-Form-Call-Sites splitten Dart-seitig bei Leerzeichen
(openWebbrowserWithSite linux.dart:917-921, openWebbrowserSeach :911-914,
cat $bookmarksLocation :957-975) — Korrektheits-Bug; und der Kommentar :1496-1499 („argv is
space-joined unquoted") ist selbst falsch.
Befund #50: drei Disk-Analyzer-Launches (linux.dart:2752/2755/2770, zweiter Einstieg
disk_space.dart:55) ohne runInShell: false; Datenfluss df → linux_filesystem.dart:68
group(6) → Mountpoint. Entkräftet als Injection (Dart-Empirie), Fix bleibt Hygiene.
Befund #46: folder_recursion_depth (linux.dart:936) ist durch den final int-Cast vor
Injection blockiert (TypeError bei String-Werten → leere Ordnerliste), bleibt aber stille
Fragilität.
Fix:
runInShell: falsean den drei Disk-Analyzer-Launches (:2752/:2755/:2770) nach dem etablierten Muster (:59/:62/:1478/:1501); Nebenbefund: :2752 feuert unconditionally vor dem Install-Check — mit reparieren.- Default-Flip
runInShell = falseinrunCommandWithCustomArguments(:106) undrunProcess(:128) — vorher alle String-Form-Call-Sites prüfen (Multi-Word-Konstanten wiecinnamon-settings userfunktionieren, darunCommandbereits Dart-seitig splittet); Kommentar :1496-1499 auf die korrekte Mechanik korrigieren. - Depth-Klemme:
int.tryParse(getValueUnsafe(...).toString()).clamp(1, 10)(oder gleiches Muster) anlinux.dart:936+ Unit-Test mit malicious Config-Wert ("'; touch /tmp/pwned). - Tests: Icon-/Analyzer-Lookups mit Apostroph-/Space-Pfaden bleiben funktional (hermetisch).
Gates: CI-Gates; manuell: Browser-Launch, Disk-Analyzer, Bookmark-Scan verhalten sich wie vorher (kein Verhaltenstrend nach dem Flip). Commits: „refactor(shell): default runInShell off in the command layer", „fix(hygiene): clamp folder_recursion_depth and pin analyzer launches".
Befund: _configuredBrowser() (app_launcher.dart:122-129) liest den Config-Key ohne
Validierung; detectBrowser() stellt den Wert der kKnownBrowsers-Allowlist voran; als Gate
dient nur _which(bin) — GNU which akzeptiert absolute Pfade → jeder which-auflösbare Wert
wird via Process.start(detached) gestartet. LOW (Persistenz-Primitiv, kein Write-Pfad).
Fix (als Teil des app_launcher.dart-Komplett-Ersatzes von V0.8.2):
trim, Allowlist-Membership (kKnownBrowsers), / ablehnen; null → sauberer Fallback.
Tests: Config-Pfad-Gruppe in test/app_launcher_test.dart (heute keine) — validierter Wert,
nicht-allowlisteter Binary-Name, Pfad-Value, trailing-space-Value (fällt still auf Fallback).
Commit fließt in den V0.8.2-Commit „fix(launcher): …" oder eigener Commit
„fix(launcher): validate preferred_browser against the known-browsers allowlist".
Befund: Sink icon_loader.dart:64-68 (--file='$filePath', drei Aufrufe :52/:59/:64) über
runInShell:true-Default; Quote-Strip-Shim in additional/python/get_icon_path_for_file.py
arbeitet unter Dart-Escaping korrekt. Entkräftet als Injection (RV-1 entscheidet final) —
bleibende Härtung: handgeschriebene Quotes sind fehleranfällig (dasselbe Muster, das
runPythonScript :1478 schon einmal abbekommen hat).
Fix (Paar!): runInShell: false an allen drei icon_loader-Aufrufen und gleichzeitig den
Quote-Strip-Shim im Python-Skript entfernen (sonst bricht der Icon-Lookup: --file='…' bleibt
dann mit Quotes im argv und der Pfad matcht nicht). RV-1-Vorlauf (Probe 1). Commit:
„fix(icon): pass file paths as plain argv to the icon helper".
additional/python/jfiles.py:181: os.system("cp '" + src + "' '" + dst + "'") →
shutil.copy2(source_path, destination_path) (Pfade als Daten; Metadaten-Erhalt passt zur
Timeshift-Config). Einziger Caller setup_automatic_snapshots.py:9 mit zwei Konstantpfaden
(latent). Regressionstest in additional/python/tests/. Upstream-Hinweis an
Jean28518/jtools-unix-python (Basti-Task, nicht Commit). Commit:
„fix(python): copy timeshift config with shutil instead of a shell command".
jessentials.py:166/175-181: URL/Pfade interpoliert in Command-Strings; run_command() macht
shlex.split + Popen ohne Shell → Whitespace-Split + Leading-Dash-Option-Injection (CWE-88).
Repo-weit 0 Caller (Dead Code), aber 18 Root-Skripte importieren jessentials (latent, LOW).
Fix: argv-Listen (oder Dead-Helper löschen — Entscheidung im Paket, Default: argv-Listen),
Leading-Dash-Guard/---Separator für link, Unit-Test nach test_command_queue.py-Schema.
Upstream-Hinweis wie #43. Commit: „fix(python): invoke wget/unzip with argv lists".
RV-1 ──→ WP-B2 (Severity-Entscheid) WP-B2, WP-B1 ── parallel zum Launch-Neubau in V0.8.2
WP-S1 ── unabhängig WP-S2 ── unabhängig (anderer Stack: Python/Queue)
WP-S3 ── nach WP-B2 empfohlen (beide fassen runInShell-Stellen an — Merge-Konflikt vermeiden)
WP-P1/P2 ── unabhängig (V0.8.3-Umfeld) #42-Comment/Close ── nach RV-1 + Basti-Sichtung
Ein Issue je Paket: [V0.8.2.5] WP-S1 … / [V0.8.2.5] WP-S2 … / [V0.8.2.5] WP-S3 … /
[V0.8.2] WP-B1 … / [V0.8.2] WP-B2 … / [V0.8.3] WP-P1+P2 … / [Vorbereitung] RV-1 …,
je mit Verdict-Kurzfassung, Fix-Skizze, Gates; Labels gate:test/gate:verify (+ security,
falls angelegt). #42 erhält das Triage-Ergebnis als Kommentar (rejected, Empirie+SDK-Quelle)
und wird nach Basti-Sichtung geschlossen; #45/#50 erhalten einen Triage-Link-Kommentar
(Severity pending RV-1), bleiben offen.
- Alle 9 Issues zugeordnet (keine Lücke): #49→WP-S1, #44→WP-S2, #42/#50/#46→WP-S3, #45→WP-B2, #47→WP-B1, #43→WP-P1, #48→WP-P2.
- RV-1 vor WP-B2; V0.8.2.5 zwischen V0.8.2 und V0.8.3; V0.8.4/V0.8.5 unangetastet.
- Jedes Paket: file:line + Tests + Gate + Commit-Messages; kein Push/PR/Version-Bump ohne Basti-OK (globale Randbedingungen).