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>
13 KiB
Bestandsaufnahme, 12.09.2026
Stand 24.09.2026. Dieses Dokument ist die Aufnahme vom 12.09. Was seither erledigt wurde, steht an Ort und Stelle durchgestrichen oder vermerkt; die Kurzfassung:
Punkte 1, 2, 2b, 3 (teilweise) ✅ erledigt Go-Tests documents,tags✅ 12 Testfunktionen Go-Tests sharing✅ 6 Testfunktionen (25.09.) Go-Tests rescan,audit,thumbnailsoffen Frontend-DOM-Tests ✅ jsdom seit 18.09., 46 Tests Dazwischen lag eine Woche Oberflächenarbeit, die hier nicht auftaucht, weil sie keine Befunde abarbeitete: Rückmeldungen mit Rückgängig, Platzhalter, Leerzustände, Sprache vereinheitlicht,
Intl. Steht in TESTING.md.
Einmal durch die ganze Cloud: Code, Sicherheit, offene Enden. Geordnet nach dem, was passieren kann — nicht nach Aufwand.
Was hier steht, ist gemessen, nicht geschätzt: Routen aus main.go
extrahiert und gegen die Rechteprüfungen in den Handlern abgeglichen,
govulncheck über den ganzen Baum, Zeilen und Testumfang je Paket gezählt.
1 · Ein Bild-Upload kann den Prozess töten
Schwere: hoch. Jeder Kontoinhaber, normale Bedienung.
cmd/aether/main.go:702 erzeugt das Vorschaubild in einer nackten
Goroutine:
go func() {
…
thumbnails.Generate(ctx, st, …) // dekodiert eine hochgeladene Datei
}()
Chis Recoverer hängt am Anfragebehandler. Eine Panik in einer eigenen
Goroutine fängt er nicht — sie nimmt den ganzen Go-Prozess mit. Docker
startet ihn neu (restart: unless-stopped), aber jeder gerade laufende Upload
stirbt mit, und wer die Datei noch einmal hochlädt, wiederholt das Spiel.
Die passende Lücke war offen: GO-2026-5061, Panik im WebP-Dekodierer bei
unstimmiger Alphakanal-Größe. Behoben am 12.09.2026 durch x/image 0.42 → 0.45
(ef60497).
Damit ist die Hälfte erledigt, nicht das Problem. Die Bauweise bleibt: Der
nächste Dekodierer-Fehler trifft genauso, und Bilddekodierer sind eine Gattung,
in der regelmäßig etwas gefunden wird. Vier weitere Goroutinen dieser Art gibt
es: internal/sharing/handler.go:232 und main.go:185 (beide nur Datenbank,
harmlos), internal/gc/gc.go:257, und der OCR-Arbeiter (go worker.Run(ctx)),
der Fremdprogramme aufruft und deren Ausgabe liest.
✅ Erledigt am 12.09.2026. pkg/bg mit zwei Funktionen:
bg.Go(name, fn)– startet eine Goroutine, die eine Panik überlebt.bg.Run(name, fn)– dasselbe ohne eigene Goroutine, für Schleifen.
Der Unterschied ist keine Feinheit: Fängt man eine Panik um den ganzen Arbeiter herum ab, überlebt der Prozess – aber der Arbeiter ist tot, und von da an wird still keine Datei mehr erkannt. Der OCR-Arbeiter sichert deshalb jeden Auftrag einzeln ab; ein unverdauliches Dokument kostet dann genau sich selbst.
Umgestellt: Vorschaubild, Bestand nachtragen, Freigabe-Zähler,
Speicherbereinigung, OCR-Arbeiter. Eine nackte Goroutine bleibt mit Begründung
im Code – die Signalbehandlung beim Herunterfahren; ein recover brächte dort
nichts, weil main ohnehin wartete, und sie fasst keine fremden Daten an.
Eine aufgefangene Panik wird mit Stapel und Herkunftsname protokolliert. Sie
ist ein Fehler und bleibt einer – sie reißt nur nichts mehr mit. Vier Tests in
pkg/bg/bg_test.go, darunter der, auf den es ankommt: Eine Schleife läuft nach
einer Panik im dritten Durchgang weiter.
Der Unterschied ist wichtig: Die Version zu heben schloss diese Lücke. Der
recoverschließt die Gattung.
2 · Beschädigte Downloads werden erkannt und verschwiegen
Schwere: mittel. Datenintegrität, still.
internal/files/handler.go:496 rechnet beim Ausliefern die Prüfsumme mit —
genau so, wie es die Projektregel verlangt („SHA-256: beim Upload berechnen,
beim Download verifizieren"). Nur passiert bei einer Abweichung nichts:
actual := hex.EncodeToString(h256.Sum(nil))
if actual != f.Checksum {
// Log corruption; headers already sent, can't change status.
// TODO: alert / observability hook
_ = actual
}
Kein Logeintrag, kein Zähler, keine Meldung. Der Kommentar sagt, was fehlt.
Dass der Statuscode nicht mehr zu ändern ist, stimmt — die Kopfzeilen sind
längst draußen. Aber still muss es deshalb nicht sein. Ein log.Error()
mit Datei-ID, erwarteter und tatsächlicher Summe kostet nichts und ist der
Unterschied zwischen „ich habe eine kaputte Datei" und „ich habe eine kaputte
Datei und weiß es seit drei Monaten nicht".
Zweitens ließe sich die Verbindung abbrechen, statt sie sauber zu beenden: Der Browser zeigt dann einen abgebrochenen Download statt einer heilen Datei mit falschem Inhalt. Das ist die ehrlichere Ausgabe.
✅ Erledigt am 12.09.2026. Beides – und ein Drittes, das dabei herauskam.
Der Logeintrag nennt Datei-ID, Speicherschlüssel, erwartete und gelesene Summe.
Dazu wird die Verbindung fallen gelassen (panic(http.ErrAbortHandler), den
chis Recoverer ausdrücklich durchreicht).
Damit das überhaupt wirkt, mussten die letzten 32 KiB zurückgehalten
werden. Sonst wäre die Datei beim Erkennen des Fehlers längst vollständig
beim Empfänger, und der Abbruch käme zu spät: Die Kopfzeilen sind draußen, die
Bytes auch. Ein tailWriter reicht deshalb alles durch außer dem Schluss; erst
wenn die Summe stimmt, darf der hinterher. Stimmt sie nicht, bekommt die
Gegenstelle weniger Bytes als im Content-Length angekündigt – und wertet das
als Fehler statt als Datei.
Geprüft in scripts/pruefsumme-selbsttest.py (14 Prüfungen): Der Test kippt
ein Bit auf der Platte, nicht im Programm, und zwar in der Mitte der Datei,
damit der Fehler nicht schon an der Länge auffällt. Zwei Größen – über und
unter den 32 KiB, denn bei einer kleinen Datei wird gar nichts herausgegeben.
Und er prüft zuerst, dass eine heile Datei vollständig durchkommt; ohne das
bewiese der Rest nichts.
2b · Und der Fund dahinter: 34 Logaufrufe schrieben ins Nichts
Schwere: hoch, indirekt. Ein Fehler, der andere Fehler versteckt.
Der neue Logeintrag blieb beim ersten Testlauf aus, obwohl der Abbruch funktionierte. Ursache:
func Ctx(ctx context.Context) *Logger {
if l, ok := ctx.Value(ctxKey{}).(*Logger); ok { return l }
else if l = DefaultContextLogger; l != nil { return l }
return disabledLogger // ← hier landete alles
}
zerolog.Ctx liefert einen abgeschalteten Logger, wenn im Kontext keiner
steckt und kein Standard gesetzt ist. Aethers middleware.Logger hängte nie
einen hinein, und DefaultContextLogger war nie gesetzt.
Alle 34 log.Ctx(...)-Aufrufe im Baum schrieben damit ins Nichts. Darunter:
- die Warnung, dass beim Zurückrollen einer Fassung ein Blob verwaist ist
(
files/handler.go,files/replace.go,handler_versions.go) - gescheiterte Suchen
- Fehler der Speicherbereinigung
- die neue Meldung über beschädigte Dateien
Das ist die unangenehmste Art Fehler: Der Aufruf sieht richtig aus, go vet
schweigt, und auffallen kann es nur, wenn man den Fehler herbeiführt, den die
Zeile melden soll.
Behoben zweifach: zerolog.DefaultContextLogger beim Start gesetzt (deckt
auch Hintergrundarbeit mit eigenem Kontext ab), und middleware.Logger hängt
zusätzlich einen Logger mit request_id in den Anfragekontext – damit bei zwei
gleichzeitigen Downloads erkennbar bleibt, welche Meldung zu welchem gehört.
Zwei Tests in pkg/middleware/logger_test.go halten es fest.
3 · Vier Pakete ohne einen einzigen Go-Test
Schwere: mittel. Nicht was heute kaputt ist, sondern was morgen still kaputtgeht.
| Paket | Zeilen | Test (24.09.) | Aussenpruefung |
|---|---|---|---|
internal/tags |
1338 | 4 ✅ | keine |
internal/documents (OCR, Volltext) |
1243 | 8 ✅ | keine |
internal/sharing |
347 | 6 ✅ | Python-Selbsttest, 31 Prüfungen |
internal/rescan |
435 | 0 | Python-Selbsttest, 20 Prüfungen |
internal/audit |
180 | 0 | keine |
internal/thumbnails |
95 | 0 | keine |
Bei sharing und rescan deckt wenigstens ein Selbsttest die Außenseite ab.
Bei documents und tags gar nichts — und documents ist das Stück, das
aus scan_0042.pdf ein auffindbares Dokument macht. Genau der Grund, aus dem
Aether das Rechnungsarchiv ist.
Teilweise erledigt am 12.09.2026: internal/documents hat jetzt acht
Tests — gezielt auf die drei Zustände, in denen die Warteschlange
hängenbleiben kann, ohne dass es auffällt (Details in
TESTING.md). Dazu scripts/db-tests.sh, damit sie nicht wie
vitest zur Dekoration werden: Sie überspringen sich ohne TEST_DSN und
liefen sonst nie.
Gegengeprüft an absichtlich kaputtem Code — und der zweite Versuch blieb
grün: maxAttempts von 3 auf 9999 gesetzt, alle Tests weiter in Ordnung,
nur die Laufzeit stieg von 0,2 auf 27 Sekunden. Die Tests schleiften über die
Konstante und setzten die Zahl voraus, statt sie zu prüfen. Dagegen steht jetzt
ein eigener Test.
internal/tags ebenfalls erledigt am 12.09.2026. Fünf Tests, alle auf
dieselbe Frage: Bleibt ein fremdes Etikett fremd – in der Liste, beim
Umbenennen, beim Löschen, beim Anhängen an die eigene Datei, und vor allem beim
Auflisten der Dateien darunter. Der letzte Weg gibt Dateinamen, Ordner und
Größen heraus; wer dort eine fremde Etikett-ID errät, sähe einen Ausschnitt aus
fremden Unterlagen.
Auch hier gegen kaputten Code gegengeprüft: Eigentümerprüfung aus
FilesWithTag entfernt → „Bert sieht 1 fremde Dateien: Gehaltsabrechnung.pdf".
Prüfung in SetFieldsForFile ausgehebelt → zwei Tests rot.
Ein Verdacht erwies sich dabei als falsch, und das ist der nützlichere
Ausgang: List bietet Gruppen-Etiketten an, SetFieldsForFile vergleicht
gegen den Prinzipal der Datei – das sah nach einem Widerspruch aus, bei dem
die Oberfläche etwas anbietet, was der Server ablehnt. Ist keiner: Die
Oberfläche filtert bereits (TagDialog.tsx:69). Und die Ablehnung ist nicht
Förmlichkeit, sondern die tragende Absicherung – FilesWithTag prüft, ob man
das Etikett sehen darf, nicht die Datei. Das ist nur deshalb harmlos,
weil ein Etikett stets denselben Prinzipal hat wie die Dateien darunter. Fiele
die Prüfung weg, hinge an einem Gruppen-Etikett eine private Datei, und jedes
Gruppenmitglied sähe ihren Namen. Der Test hält jetzt diesen Zusammenhang fest,
nicht bloß eine Fehlermeldung.
Nebenbei: internal/tags hat zwei Fehlermarken für dieselbe Aussage –
ErrNotFound und ErrCorrespondentNotFound. Kein Fehler, aber eine
Stolperstelle; jetzt wenigstens festgehalten.
Offen bleiben internal/sharing, internal/rescan (beide mit
Python-Selbsttest), internal/audit und internal/thumbnails.
4 · Was in Ordnung ist
Der Vollständigkeit halber, weil eine Liste nur aus Mängeln ein falsches Bild gibt:
- Alle 27 Admin-Routen prüfen serverseitig auf Adminrecht. Zwei
Ausnahmen sind Absicht und dokumentiert:
GET /admin/settingsliest jeder Angemeldete (Theming — und es liegen dort nur sechs Branding-Schlüssel, nachgesehen),GET /ocrist der eigene Erkennungsstand ohne Adminrecht. - Kein einziges zusammengesetztes SQL. Alles parametrisiert.
- Inline-Auslieferung nur über eine Positivliste harmloser Typen, mit
sandbox, undallow-scriptsausschließlich für PDF. - Ratenbegrenzung an jeder unangemeldeten Tür, die eines braucht: Anmeldung
(20/min), WebDAV, CalDAV, Freigabelinks (je 10 Fehlversuche / 15 Minuten).
Die übrigen offenen Wege (
/health,/branding,/manifest.webmanifest,/api/v1/public/config) geben nichts preis. - Kein
dangerouslySetInnerHTML, keinevalim Frontend. - Sechs Selbsttest-Suiten, 220 Prüfungen gegen die laufende Instanz, dazu 37 Frontend-Tests und die Go-Tests.
5 · Kleinere offene Enden
-
internal/groupsist ein leerer Stub (0 Zeilen). Gruppenordner liegen infiles/groupfolder.go. Entweder das Verzeichnis weg oder der Code dorthin — ein leeres Paket im Baum ist eine Frage, die jeder einmal stellt. -
favicon_storage_keysteht als leere Zeile in den Einstellungen; die Funktion heißt seit dem App-Symbol anders. Schadet nichts, wird ignoriert. -
Die Fehlversuchssperren liegen im Arbeitsspeicher. Ein Neustart des Containers hebt jede Sperre auf. Für den Hausgebrauch vertretbar; wer es anders will, braucht eine Tabelle.
-
Frontend ohne Komponententests.✅ 18.09.2026:jsdomist in den devDependencies, die Umgebung bleibtnode(schnell); wer ein DOM braucht, schreibt// @vitest-environment jsdoman den Anfang seiner Datei. 46 Tests, davon vier gegen ein DOM. Der erste davon hat sofort einen Fehler gefunden – in der Behebung vom Vortag (useBackClose).Was er nicht leistet, steht dabei: Den ursprünglichen Fehler bildet jsdom nicht ab; es verwirft einen Verlaufssprung, sobald
pushStatefolgt. Dafür bräuchte es einen echten Browser (Playwright). Nachgeprüft, nicht vermutet: mit der alten, kaputten Fassung blieben alle Tests grün.
Reihenfolge, die ich vorschlagen würde
✅ 12.09.2026recoverin die Hintergrund-GoroutinenPrüfsummen-Abweichung melden✅ 12.09.2026 – und dabei fiel auf, dass alle kontextgebundenen Logzeilen ins Nichts gingen (2b).Tests für✅ unddocuments✅ 12.09.2026.tags