Sicherheits-Audit — Juli 2026 — Runde 6
Code-Security-Review
NetMon Roots v1.1.16
Sechstes automatisiertes Security-Review auf Basis von
Claude Fable 5. Schwerpunkt dieser Runde: die seit Runde 5
hinzugekommene Favicon-Download-Netzwerkfunktion des
Secure Browser Managers
(fetch_url(), resolve_favicon_download(),
extract_icon_links() in
browser_container_builder.py) — die erste ausgehende
Netzwerk-Schnittstelle des Features — sowie die
Pfad-Confinement-Härtung des Coding-Agenten
(agent/runner.py) und das damals noch vorhandene
Dev-Container-GUI-/Docker-Profil
(.devcontainer/gui-docker/, inzwischen entfernt). Anders als in Runde 5
wurden keine ausnutzbaren Schwachstellen bestätigt:
vier Kandidaten wurden von dedizierten Validierungs-Sub-Agenten allesamt
unterhalb der Konfidenzschwelle eingestuft (2–3 / 10).
Drei davon wurden dennoch als
Defense-in-Depth proaktiv gehärtet und mit neuen
Regressionstests abgesichert.
Umfang & Methodik
Schwerpunkt dieser Runde war die seit dem Runde-5-Snapshot neu
hinzugekommene bzw. veränderte Angriffsfläche im
Arbeitsstand: die erstmals eingeführte Favicon-Download-Funktion des
Secure Browser Managers (ausgehende HTTP-Requests aus
Homepage-URL und aus dem HTML geparsten Icon-Links), die
Pfad-Confinement-Logik des Coding-Agenten
(_looks_like_path_argument() /
_validate_repo_confined_arguments()) sowie die neu
hinzugefügten Dev-Container-Profile. Die in Runde 5 behobenen
Findings (K1–K3) wurden zusätzlich am aktuellen Code
gegengeprüft — die Fixes sind unverändert wirksam
(CONTAINER_NAME_PATTERN, json.dumps()-Escaping,
_desktop_field()).
Phase 1 — Diff- & Reconnaissance
Vollständiger git diff gegen den letzten Commit plus
Sichtung aller neuen unversionierten Dateien; gezieltes Lesen der neuen
Netzwerkpfade in browser_container_builder.py und der
Härtungslogik in agent/runner.py. Abgleich gegen die im
README dokumentierten Sicherheitsgarantien.
Phase 2 — Kandidaten-Analyse
Ein dedizierter Analyse-Sub-Agent verfolgte Datenflussketten von
Nutzer-/Konfig-Eingaben (Containername, homepage_url,
Agent-Kommandozeile) bis zu sensiblen Operationen
(urlopen, mkdir/copytree,
subprocess.run). 4 Kandidaten identifiziert.
Phase 3 — False-Positive-Validierung
Für jeden Kandidaten lief ein eigener, paralleler Validierungs-Sub-Agent mit striktem FP-Filter-Regelwerk und Konfidenz-Scoring. Ergebnis: alle vier unter der Schwelle (2–3 / 10) — kein Angriffspfad über eine echte Vertrauensgrenze in einem Einzelnutzer-Desktop-Werkzeug.
Phase 4 — Härtung & Regressionscheck
3 der 4 Punkte als Defense-in-Depth umgesetzt und mit
6 neuen Regressionstests abgesichert. Bestehende Suiten erneut
ausgeführt: test_runner.py,
test_secure_browser_module.py und
test_browser_container_builder.py —
149 Tests, alle grün.
Sicherheitsfokus (Runde 6)
| Datei / Modul | Fokus | Ergebnis |
|---|---|---|
browser_container_builder.py — fetch_url() |
Ausgehende HTTP(S)-Fetches ohne Scheme-Restriktion
(file:// via Default-Opener erreichbar) |
K1 → gehärtet |
browser_container_builder.py — resolve_favicon_download() / extract_icon_links() |
SSRF-Betrachtung: homepage_url und aus HTML geparste
Icon-Links steuern Ziel-Host |
Kein Finding (Self-SSRF) |
agent/runner.py — _looks_like_path_argument() |
Pfad-Confinement für pytest/black/ruff/ls;
--flag=value-Argumente wurden nicht geprüft |
K2 → gehärtet |
browser_container_builder.py / manager.py — Namensvalidierung |
CONTAINER_NAME_PATTERN matcht weiterhin
./.. |
K3 → gehärtet |
netmon/app.py — _is_netmon_listener() |
Substring-Match "netmon" in process_name zur
Listener-Zuordnung in der Firewall-Transparenz |
K4 → Empfehlung (offen) |
.devcontainer/gui-docker/devcontainer.json |
Historisches offenes Entwickler-Profil mit
docker.sock- und X11-Mount; inzwischen aus dem
aktuellen Setup entfernt |
Historisch / entfernt |
| Runde-5-Fixes (K1–K3) | Gegenprüfung, ob die Patches am aktuellen Code weiterhin greifen | Weiterhin wirksam |
Außerhalb des Scopes: die im Repository-Root liegende
index.html ist eine fremde Drittanbieter-Seite (Rootkit-Hunter-
Projekt) und gehört nicht zur ausgelieferten Anwendung —
empfohlen wird deren Entfernung/Verschiebung als reine Aufräum-Maßnahme.
Das frühere GUI-/Docker-Dev-Container-Profil mit Host-Docker-Socket-Mount
war ein bewusst offenes Entwickler-Feature, wurde aber aus dem aktuellen
Setup entfernt und ist kein Bestandteil der ausgelieferten App.
Bewertete Kandidaten (alle unterhalb der Konfidenzschwelle)
Vier Kandidaten wurden identifiziert und jeweils von einem eigenen
Validierungs-Sub-Agenten gegengeprüft. Keiner erreichte die
Konfidenzschwelle von 8/10 für ein bestätigtes, ausnutzbares
Finding — im Wesentlichen, weil es sich um ein
Einzelnutzer-Desktop-Werkzeug handelt, bei dem die vermeintliche
„Angreifer“-Eingabe (z. B. selbst geschriebene
containers.json oder lokal getippte Agent-Kommandos) vom
selben Nutzer stammt, der das Werkzeug ausführt. Drei der vier
Punkte wurden dennoch als sinnvolle Defense-in-Depth gehärtet.
K1 — Ausgehende Netzwerk-Fetches ohne Scheme-Allowlist (Favicon-Download)
Konfidenz 3/10 GehärtetDatei: tools/secure_browser_manager/browser_container_builder.py
— fetch_url(), aufgerufen aus
resolve_favicon_download() (Standard-Icon-Modus
favicon).
fetch_url() reichte homepage_url (und daraus per
urljoin() abgeleitete Icon-Links) ohne Scheme-Prüfung an
urllib.request.urlopen(). Der Default-Opener von urllib
behandelt neben http/https auch
file:// — ein homepage_url wie
file:///etc/passwd hätte damit theoretisch einen lokalen
Dateizugriff ausgelöst, und ein beliebiger Host wäre als
SSRF-Primitive erreichbar gewesen.
Warum unter der Schwelle: In diesem Einzelnutzer-Desktop-
Werkzeug wird homepage_url ausschließlich vom selben
Nutzer gesetzt, der auch --apply ausführt
(lokale containers.json bzw. GUI-Eingabe). Es existiert kein
Remote-/Mehrbenutzer-Eingabepfad und kein Import-aus-URL-Feature —
also keine echte Vertrauensgrenze, die überschritten wird
(Self-SSRF / selbstverschuldete Fehlkonfiguration).
fetch_url() prüft
jetzt das Scheme und lehnt alles ab, was nicht http/https
ist — damit ist der file://-Pfad und jede andere
Nicht-Web-Scheme geschlossen, bevor die URL den Opener erreicht. Neuer
Regressionstest test_fetch_url_rejects_non_http_schemes
(file://, ftp://, gopher://,
absoluter Pfad) verifiziert die Ablehnung.
K2 — --flag=value-Lücke im Pfad-Confinement des Coding-Agenten
Konfidenz 3/10
Gehärtet
Datei: agent/runner.py —
_looks_like_path_argument() /
_validate_repo_confined_arguments().
Die in Runde 5 eingeführte Repo-Confinement-Prüfung
behandelte jedes Argument, das mit - beginnt, sofort als
„kein Pfad“ und übersprang es. Flags der Form
--junitxml=/pfad, --basetemp=/pfad oder
--output-file=/pfad (bei pytest/ruff)
trugen ihren Pfad also am Confinement-Check vorbei — ein Schreibziel
außerhalb des Repos wäre nicht abgewiesen worden.
Warum unter der Schwelle: Der einzige Aufrufer
(agent/cli.py) baut das Kommando aus sys.argv —
es ist ein manuell vom Betreiber gestartetes lokales CLI, keine an
untrusted Input angebundene autonome LLM-Tool-Schleife. Damit greift der
Grundsatz „vom vertrauenswürdigen Betreiber übergebene
CLI-Argumente sind kein Angreifer-Input“. Die Absicht des
Confinement-Codes wird dennoch vervollständigt.
_looks_like_path_argument()
zerlegt jetzt --flag=value-Argumente und prüft den
Wert-Teil; eine neue Hilfsfunktion _flag_value() sorgt
dafür, dass auch _is_repo_confined_path() den reinen
Pfad-Teil bewertet. Neue Tests
(test_run_command_rejects_pytest_flag_value_outside_repo,
...ruff..., ...allows_flag_value_inside_repo)
bestätigen, dass Pfade außerhalb des Repos abgewiesen und solche
innerhalb weiterhin erlaubt werden.
K3 — . / .. passieren die Containername-Validierung
Konfidenz 2/10
Gehärtet
Datei: tools/secure_browser_manager/browser_container_builder.py
(validate_config_payload()) und
netmon/modules/secure_browser/manager.py
(_resolve_create_inputs()).
Das Runde-5-Muster ^[A-Za-z0-9_.-]+$ erlaubt den Punkt und
matcht damit auch die Literale . und ...
base_dir / name zeigt für .. lexikalisch auf
das übergeordnete Verzeichnis.
Warum unter der Schwelle: In der Praxis blockiert die
bestehende mkdir(parents=True, exist_ok=False)-Semantik den
Schreibvorgang — ein empirischer Test bestätigte, dass bei
name=".." zuerst base_dir selbst angelegt wird und
der finale mkdir dann zuverlässig mit
FileExistsError abbricht, bevor copytree
erreicht wird. Kein Schreibzugriff außerhalb von
base_dir heute möglich.
name in {".", ".."} jetzt explizit mit klarer Fehlermeldung
abgelehnt — die Namensprüfung ist damit selbst fail-closed und
hängt nicht mehr an der zufälligen mkdir-Semantik.
Neue Tests test_validate_config_payload_rejects_dot_and_dotdot_names
und ...rejects_path_traversal_name sichern das ab.
K4 — Loser Substring-Match bei der NetMon-Listener-Zuordnung
Konfidenz 3/10 Empfehlung (offen)Datei: netmon/app.py —
_is_netmon_listener().
Die Firewall-Transparenz stuft einen Listener als „gehört zu
NetMon selbst“ ein, u. a. per
if "netmon" in process_name. Ein lokal laufender Prozess mit
einem Namen wie netmon-helper würde damit als eigener
Listener gelabelt.
Warum unter der Schwelle & bewusst nicht geändert:
Die Klassifikation dient rein der Anzeige/Priorisierung in einem
informativen Diagnose-Report — sie steuert keine
ufw-Regel und blendet keine Zeile aus der Haupttabelle aus
(Prozessname, PID, Port bleiben sichtbar). Ein Angreifer, der einen Prozess
frei benennen kann, hat bereits lokale Codeausführung als der Nutzer;
das Verbergen einer Zeile in einer Zusammenfassung ist keine relevante
Eskalation. Ein Entfernen des Matches birgt zudem das Risiko, echte
NetMon-Listener fälschlich als „fremd“ zu markieren
(schlechtere UX). Daher bewusst als Empfehlung belassen.
pid == os.getpid() und den systemd-Scope-Check
_is_netmon_app_scope() (app-gnome-netmon-*.scope)
— und den reinen Substring-Match entfernen, sobald die
Selbst-Erkennung der eigenen Listener über diese Pfade verlässlich
abgedeckt ist.
Umgesetzte Härtungsmaßnahmen (Runde 6)
Auch ohne bestätigtes ausnutzbares Finding wurden die drei risikoreduzierenden Kandidaten direkt geschlossen — jeweils fail-closed und mit Regressionstest.
Scheme-Allowlist für Netzwerk-Fetches — browser_container_builder.py
fetch_url() akzeptiert nur noch http/https.
Damit sind file:// (lokaler Dateizugriff via urllib-
Default-Opener) und andere Nicht-Web-Schemes ausgeschlossen, bevor die URL
urlopen() erreicht.
Vollständiges Pfad-Confinement inkl. --flag=value — agent/runner.py
_looks_like_path_argument() zerlegt jetzt
--flag=value-Argumente und prüft den Wertteil; die neue
Hilfsfunktion _flag_value() stellt sicher, dass auch
_is_repo_confined_path() den reinen Pfad bewertet. Schreibziele
wie --junitxml=/etc/passwd werden nun abgewiesen.
Fail-closed Containername-Validierung — browser_container_builder.py + manager.py
. und .. werden an beiden
Validierungseinstiegen explizit abgelehnt — die Namensprüfung
hängt nicht mehr an der zufälligen
mkdir(exist_ok=False)-Semantik als letzter Schutzlinie.
6 neue Regressionstests — 149 / 149 grün
test_runner.py (+3: --flag=value innerhalb/
außerhalb Repo), test_browser_container_builder.py
(+3: ./..-Namen, Pfad-Escape-Name, Nicht-HTTP-
Schemes). Alle drei relevanten Suiten bestehen nach den Fixes vollständig.
Runde-5-Fixes — am aktuellen Code gegengeprüft
Die drei in Runde 5 behobenen Findings wurden nicht nur dem Dokument entnommen, sondern am aktuellen Quelltext verifiziert — alle Patches sind unverändert wirksam.
K1 (R5) — Namensvalidierung
CONTAINER_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+$")
vorhanden in browser_container_builder.py:35 und
manager.py:54, an beiden Einstiegen angewandt — in
Runde 6 zusätzlich um die ./..-Ablehnung
erweitert.
K2 (R5) — user.js-Escaping
homepage_url/new_tab_url werden weiterhin per
json.dumps() kodiert
(browser_container_builder.py:1312–1313) —
Preference-Injection bleibt geschlossen.
K3 (R5) — .desktop-Feld-Bereinigung
_desktop_field() entfernt weiterhin \r/\n
und wird an allen drei Launcher-Erzeugungsstellen verwendet
(browser_container_builder.py:1463 ff.).
Geprüfte Sicherheitskategorien
| Kategorie | Geprüft | Ergebnis |
|---|---|---|
| SSRF / ausgehende URL-Fetches (Host-/Scheme-Kontrolle) | Ja | K1 → gehärtet (Scheme-Allowlist) |
Local File Read via file:// im URL-Opener |
Ja | K1 → gehärtet |
| Path Traversal (Dateioperationen mit Nutzereingabe) | Ja | K3 → gehärtet (fail-closed) |
| Agent-Sandbox: Pfad-Confinement der Kommando-Allowlist | Ja | K2 → gehärtet (--flag=value) |
Command Injection (subprocess, shell=True) |
Ja | Kein Finding |
| Argument Injection (list-form subprocess mit externen Daten) | Ja | Kein Finding |
Firefox user.js/Preference-Injection |
Ja | Geschlossen (R5-K2) |
Desktop-Entry-Injection (.desktop Name=/Comment=) |
Ja | Geschlossen (R5-K3) |
| Unsichere Deserialisierung (pickle, yaml.load, eval) | Ja | Kein Finding |
| Privilege Escalation (historisches Dev-Container docker.sock-Mount) | Ja | Historisch / entfernt |
| Sicherheitskontroll-Umgehung (Listener-Zuordnung) | Ja | K4 → Empfehlung (offen) |
| Hardcoded Credentials / API-Keys | Ja | Kein Finding |
| Sensitive Data Exposure (PII in Logs) | Ja | Kein Finding |
Auditor: Claude Fable 5 (Anthropic) —
Analyse-Sub-Agent + vier parallele False-Positive-Validierungs-Sub-Agenten
+ manuelle Fix-Umsetzung und Regressionstests
Projektverantwortlicher: Sven Fröhlich
Datum: 07. Juli 2026
Analysierter Codestand:
9baa6284 + Arbeitsstand (modified/untracked), Fokus
Favicon-Download & Agent-Pfad-Confinement — Runde 6
Methode: Statische Code-Analyse + manuelle Datenfluss-Verifikation
+ Konfidenz-gescorte FP-Validierung + Regressionstests
(149/149 relevante Tests bestanden nach den Härtungen)
— kein Laufzeit-Penetrationstest
Haftungsausschluss: Dieses Audit ersetzt keinen professionellen
Penetrationstest durch eine zertifizierte Sicherheitsfirma. Die Analyse
beschränkt sich auf den angegebenen Codestand. Nicht erfasst werden:
Laufzeitverhalten, abhängige Systembibliotheken,
Netzwerkkonfiguration und soziale Angriffsvektoren.