Files
GuidoandClaude Opus 5 1b47e5abe1 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>
2026-08-27 15:07:42 +02:00

96 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import date
from conftest import login_ready, new_user
def test_users_are_isolated(admin):
# Admin legt ein Konto an.
acc = admin.post("/api/accounts", json={"name": "AdminKonto", "currency": "EUR"}).json()
# Frischen Nutzer anlegen und getrennt einloggen.
username, password = new_user(admin)
other, _ = login_ready(username, password)
# Sieht keine fremden Konten/Buchungen.
assert other.get("/api/accounts").json() == []
assert other.get("/api/transactions").json() == []
# Zugriff auf fremdes Konto -> 404 (nicht 403: existiert für ihn schlicht nicht).
assert other.get(f"/api/accounts/{acc['id']}").status_code == 404
# Buchung auf fremdes Konto -> 404.
r = other.post("/api/transactions", json={
"type": "expense", "date": str(date.today()), "account_id": acc["id"], "amount_cents": 100,
})
assert r.status_code == 404
# Eigene Startkategorien sind vorhanden.
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