fix: Ohne Anmeldung liessen sich beliebige Dateien vom Server lesen

Die Auslieferung des Frontends nahm den Pfad roh aus der Adresse: "Path /
'../../x'" verlaesst den Ordner klaglos, und die Route liegt VOR jeder
Anmeldepruefung. Gegen die eigene Instanz nachgestellt – roh und
prozentkodiert kam beides Mal die Datei heraus:

    GET /../../GEHEIM.txt            -> "geheim"
    GET /../../backend/app/config.py -> der Quelltext

Damit war alles lesbar, was der Dienst lesen darf: Quelltext, Konfiguration,
und mit der passenden Zahl von Ebenen die Datenbank samt verschluesseltem
Postfach-Passwort und Bank-Schluessel. Der Pfad wird jetzt aufgeloest und
muss im Auslieferungs-Ordner liegen.

Zweitens prueft der Postfach-Abruf jetzt das Zertifikat. Ohne eigenen
SSL-Kontext tut imaplib das NICHT – die Verschluesselung schuetzt dann vor
niemandem, der sich dazwischenhaengt, und das Postfach-Passwort geht mit.
Gilt fuer beide Wege, SSL und STARTTLS.

Drittens warnt der Start, wenn das Sitzungs-Cookie ohne "Secure" ausgeliefert
wird. Die Vorgabe bleibt bewusst aus: Sie umzustellen sperrt jeden aus, der
Certain im Heimnetz ueber HTTP aufruft – und ein Ausgesperrter saehe die
Ursache nirgends.

Die Befunde stammen aus dem /security-review dieser Sitzung und waren seither
offen. Sie wiegen schwerer, seit die Anwendung fuer die Bankanbindung unter
einer oeffentlichen HTTPS-Adresse erreichbar sein muss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-27 15:07:42 +02:00
co-authored by Claude Opus 5
parent d88967d77d
commit 1b47e5abe1
3 changed files with 101 additions and 6 deletions
+68
View File
@@ -25,3 +25,71 @@ def test_users_are_isolated(admin):
assert len(other.get("/api/categories").json()) >= 6
# Kein Admin: darf keine Nutzer listen.
assert other.get("/api/users").status_code == 403
def test_kein_pfadwechsel_aus_dem_auslieferungs_ordner(client, tmp_path):
"""Diese Route liegt VOR jeder Anmeldepruefung. Ohne Begrenzung liefert
"/../../etc/passwd" genau das aus – der Pfad kommt roh aus der Adresse,
und "Path / '../x'" verlaesst den Ordner klaglos."""
from app import main
if not main.DIST_DIR.exists():
import pytest
pytest.skip("kein gebautes Frontend vorhanden")
geheim = main.DIST_DIR.parent.parent / "PRUEF-GEHEIM.txt"
geheim.write_text("streng geheim", encoding="utf-8")
try:
for pfad in ("/../../PRUEF-GEHEIM.txt",
"/%2e%2e%2f%2e%2e%2fPRUEF-GEHEIM.txt",
"/../../backend/app/config.py"):
r = client.get(pfad)
assert "streng geheim" not in r.text, f"{pfad} gab die Datei heraus"
assert "APP_DATA_DIR" not in r.text, f"{pfad} gab Quelltext heraus"
finally:
geheim.unlink(missing_ok=True)
def test_echte_dateien_werden_weiterhin_ausgeliefert(client):
"""Die Begrenzung darf die Auslieferung nicht kaputtmachen."""
from app import main
if not (main.DIST_DIR / "index.html").exists():
import pytest
pytest.skip("kein gebautes Frontend vorhanden")
assert client.get("/").status_code == 200
assert "<!doctype html" in client.get("/irgendeine/spa/route").text.lower()
def test_imap_prueft_das_zertifikat(monkeypatch):
"""Ohne eigenen Kontext prueft imaplib das Zertifikat NICHT – die
Verschluesselung schuetzt dann vor niemandem, der sich dazwischenhaengt,
und das Postfach-Passwort geht mit."""
import ssl
from app.services import mail_import
gesehen = {}
class FakeSSL:
def __init__(self, host, port, ssl_context=None, **kw):
gesehen["context"] = ssl_context
def login(self, *a, **kw):
return "OK", []
def __getattr__(self, name):
return lambda *a, **kw: ("OK", [])
monkeypatch.setattr(mail_import.imaplib, "IMAP4_SSL", FakeSSL)
monkeypatch.setattr(mail_import.geheimnis, "entschluesseln", lambda x: "pw")
class Quelle:
password_enc, host, port, security = "x", "imap.example", 993, "ssl"
username = "wer@example.com"
mail_import._verbinden(Quelle())
kontext = gesehen["context"]
assert kontext is not None, "ohne Kontext prueft imaplib nichts"
assert kontext.verify_mode == ssl.CERT_REQUIRED
assert kontext.check_hostname is True