security(sharing): Freigabelink prüft die Prüfsumme, Gruppenordner teilbar

Beim Lesen von internal/sharing fiel eine ungleiche Zusage auf: Der
angemeldete Download prüft seit dem 12.09. die SHA-256-Summe beim Ausliefern
und lässt die Verbindung fallen, wenn sie nicht stimmt. Der öffentliche
Freigabelink lief durch ein blankes io.Copy – ungeprüft.

Ausgerechnet der Weg nach draussen. Wer die Datei angemeldet holt, ist der
Eigentümer und merkt, wenn etwas kaputt ist; wer einen Link bekommt, merkt es
nicht. In CLAUDE.md steht das unter „nicht verhandelbar": beim Upload
berechnen, beim Download verifizieren. Die Regel galt an einer von zwei
Stellen. Die Mechanik liegt jetzt in files/verified.go und wird von beiden
gerufen. Nachgewiesen mit gekipptem Bit auf der Platte: Über den Freigabelink
kommt die Datei ebenfalls nicht mehr vollständig an.

Gruppenordner: Create übergab user.ID statt des aufgelösten Prinzipals – eine
Datei im Gruppenordner liess sich nicht verlinken, auch nicht vom Mitglied.
Marcs Entscheidung: Das soll gehen, der Empfänger sieht ohnehin nur die Datei,
nie den Ordner darum. Verlangt wird Schreibrecht: Einen Link zu erzeugen
heisst, die Datei öffentlich zu machen; wer sie nur geliehen lesen darf, soll
sie nicht weiterreichen können (403).

expires_in_hours hatte keine Obergrenze. time.Duration rechnet in
Nanosekunden; ab etwa 2,6 Mio Stunden läuft int64 über, die Dauer wird negativ,
und der Link ist abgelaufen in dem Moment, in dem er entsteht – ohne
Fehlermeldung. Jetzt zehn Jahre, darüber 400.

Dazu X-Content-Type-Options: nosniff, das der angemeldete Weg längst setzt.

Sechs Go-Tests für internal/sharing, unter Mutation nachgewiesen:
Eigentümerprüfung entfernt, Ablaufprüfung entfernt und Token verkürzt machen
jeweils genau den zugehörigen Test rot.

Selbsttests erweitert: sharing 24 → 31 Prüfungen, pruefsumme 14 → 16.

Geprüft: go build/vet sauber, alle Go-Pakete grün, 241 Prüfungen der sieben
Selbsttest-Suiten grün, 48 Vitest-Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-25 17:57:10 +02:00
1 parent a7528d7365
commit 6fab2388ca
9 files changed
+628 -74

No files matched your search

+35
View File
@@ -92,6 +92,25 @@ def herunterladen(file_id):
return False, None, type(e).__name__ + ": " + str(e)[:80]
def oeffentlich_holen(token):
"""Dasselbe ueber den Freigabelink – ohne Anmeldung.
Bis zum 25.09.2026 lief hier ein blankes io.Copy: Wer die Datei ueber
einen Link bekam, bekam sie ungeprueft. Ausgerechnet auf dem Weg, auf dem
sie das Haus verlaesst.
"""
req = urllib.request.Request(BASE + f"/share/{token}/download",
data=b"{}", method="POST",
headers={"Content-Type": "application/json"})
try:
with urllib.request.build_opener().open(req) as r:
erwartet = int(r.headers.get("Content-Length", "-1"))
daten = r.read()
return (erwartet < 0 or len(daten) == erwartet), daten, ""
except Exception as e:
return False, None, type(e).__name__ + ": " + str(e)[:80]
def blobpfad(name):
"""Wo der Blob liegt.
@@ -118,9 +137,12 @@ assert st == 200, f"Anmeldung fehlgeschlagen: {st}"
CSRF = {"X-CSRF-Token": login["csrf_token"]}
angelegt = []
geteilt = []
def aufraeumen():
for lid in geteilt:
api("DELETE", f"/api/v1/shares/{lid}", None, CSRF)
for fid in angelegt:
api("DELETE", f"/api/v1/files/{fid}", None, CSRF)
st, papier = api("GET", "/api/v1/trash", None, CSRF)
@@ -168,6 +190,19 @@ try:
check("und was ankam, ist kuerzer als angekuendigt", len(daten) < groesse,
f"{len(daten)} von {groesse}")
# 5. Und jetzt derselbe Weg nach draussen: ueber einen Freigabelink.
# Die Datei ist dieselbe und immer noch kaputt – die Zusage muss
# hier genauso gelten.
st, link = api("POST", f"/api/v1/files/{datei['id']}/share", {}, CSRF)
if st == 201 and link:
geteilt.append(link["id"])
voll, daten, fehler = oeffentlich_holen(link["token"])
check("auch ueber den Freigabelink kommt sie NICHT vollstaendig an",
not voll,
f"{len(daten or b'')} von {groesse} Bytes – heil ausgeliefert!")
else:
check("Freigabelink angelegt", False, st)
# 4. Der Server muss es gesagt haben.
log = logzeilen(marke)
if log: