Skip to content

OpenGewerk 0.6.3

Choose a tag to compare

@github-actions github-actions released this 29 Oct 22:46
· 122 commits to main since this release
v0.6.3
e0603e6

Diese Fassung enthält eine Sicherheitskorrektur. Was betroffen war, steht im Abschnitt „Sicherheit“ und im Advisory. Wer OpenGewerk betreibt, aktualisiert bitte bald.

Eine Sicherheitsfassung mit den Korrekturen aus der Sicherheitsprüfung der Handwerkersoftware vom 27. bis 29.10.2026: 16 bestätigte Befunde, keiner kritisch oder hoch, und 29 Hinweise, die behoben oder gehärtet sind. Sie betreffen vor allem Belege, die beim Ausstellen und Stornieren jetzt gesperrt werden, den Abgleich, die Anmeldung mit zweitem Faktor und Grenzen für alles, was sich die Betriebe einer Instanz teilen. Einer, die Bestätigung einer neuen E-Mail-Adresse, steht noch aus (#754).

Das Update meldet einmal jedes Gerät ab, weil die Cookies der Anmeldung jetzt __Host-better-auth.* heißen; danach meldet sich jeder neu an. Wer einen Passkey als zweiten Faktor hat und sich mit dem Passwort anmeldet, richtet daraus keine App mehr ein und ändert nichts an seinen Passkeys, sondern meldet sich dafür mit dem Passkey an. Neu im Bereich der Instanz sind unter „Einstellungen“ die Grenzen eines Betriebs, ab Werk ohne Grenze für den Speicher. Das Installationspaket ist jetzt signiert und lässt sich vor dem Entpacken mit cosign verify-blob prüfen.

Aktualisiert wird wie bei jeder Fassung, vorher gesichert. Sechs Migrationen kommen dazu, 0089_line_waits_for_its_document, 0090_sign_only_a_report, 0091_document_bounds, 0092_audit_value_nonces, 0093_second_factor_changes und 0094_tenant_limits.

Sicherheit

  • Im Bereich der Instanz gibt es unter „Einstellungen“ die Karte „Betriebe“: „Speicher je Betrieb“ ist eine Grenze in GB für alles, was ein Betrieb im Dateispeicher ablegt, ab Werk ohne Grenze; „Inhaber dürfen weitere Betriebe anlegen“ ist ab Werk an, und ein Konto legt höchstens fünf eigene Betriebe an. Ein Betrieb an seiner Grenze bekommt beim Hochladen 507 mit einem Satz, alle anderen legen weiter ab. Die Liste der Betriebe zeigt in der neuen Spalte „Belegt“, wie viel jeder belegt. Beide Einstellungen stehen in der Migration 0094_tenant_limits. Warum: Der Dateispeicher ist einer für alle Betriebe der Instanz, und nur die freie Reserve für Datenbank, Drucke und Sicherung begrenzte, was einer behält; der erste Betrieb, der sie erreichte, sperrte das Hochladen für alle anderen. Und jeder Inhaber konnte beliebig viele eigene Betriebe anlegen, die keiner löscht und die jeden Durchgang der Hintergrundläufe und jeden Start der Instanz verlängern.
  • Hat ein Konto einen zweiten Faktor, ändert eine Anmeldung ohne ihn nichts mehr daran: eine App einrichten, einen Passkey hinzufügen oder löschen und sich unter „Konto“ bestätigen, etwa für Name und E-Mail, geht nur noch aus einer Sitzung mit dem Passkey oder dem Code aus der App, sonst antwortet der Server mit 403. Das Tor schickt ein solches Konto zur neuen Anmeldung statt zur Einrichtung der App. Wer seinen einzigen zweiten Faktor verloren hat, bekommt ihn vom Inhaber unter „Zugänge“ zurückgesetzt („Faktor zurücksetzen“), ein Konto, das auch in einem anderen Betrieb arbeitet oder die Instanz betreibt, mit reset-second-factor auf dem Server; die App und alle Passkeys gehen, alle Geräte werden abgemeldet, das Passwort bleibt. Das Einrichten einer App und jedes Zurücksetzen stehen als Zeile der neuen Tabelle second_factor_changes (Migration 0093_second_factor_changes) im Änderungsprotokoll jedes Betriebs, in dem der Zugang arbeitet, und das Konto bekommt eine Mail. Warum: Ein Konto, dessen zweiter Faktor ein Passkey ist, meldet sich auch mit dem Passwort allein an, und aus einer solchen Sitzung ließ sich eine App einrichten, deren Sitzung dann jede leitende Rolle und den Bereich der Instanz öffnete, ohne dass der Passkey je vorgezeigt wurde; die Einrichtung der App stand nirgends und meldete sich nicht. Dabei fiel auf, dass die Liste der Zugänge ein Konto, dessen zweiter Faktor ein Passkey ist, nie als „zweiter Faktor eingerichtet“ zeigte: Ihre Abfrage verglich den Passkey mit seiner eigenen Kennung statt mit dem Konto.
  • Wer einen Passkey löscht oder neue Wiederherstellungscodes erzeugt, meldet damit alle anderen Geräte des Zugangs ab, wie mit einem neuen Passwort; was sie in einem Mandanten offen hatten, wird geschlossen. Warum: Eine Sitzung hält nicht fest, welcher Passkey oder welcher Code ihr den zweiten Faktor gab, und eine, die mit dem gelöschten Passkey oder einem Code des alten Satzes begann, kam so weiter in jede Rolle, die einen zweiten Faktor verlangt, solange sie benutzt wurde. Die Dialoge unter „Konto“ sagen es dazu.
  • Der Cursor eines Abrufs geht nicht mehr über die Nummer hinaus, die der Zähler des Mandanten vor der ersten Tabelle zeigte, und eine Übertragung des Postausgangs trägt höchstens 2000 Vorgänge (413 darüber); der Web-Client teilt seinen Postausgang zusätzlich in Teile von höchstens 1000. Warum: Bestätigte während eines Abrufs eine Transaktion Zeilen in zwei Tabellen, sah der Abruf nur die höhere Nummer, und das Gerät bekam die niedrigere nie; und acht Megabyte der kleinsten Vorgänge waren Zehntausende Zeilen in einer Transaktion, die den Zähler des Mandanten hält.
  • Die Cookies der Anmeldung heißen __Host-better-auth.* statt __Secure-better-auth.*. Das Update meldet einmal jedes Gerät ab, danach meldet sich jeder neu an. Warum: Ein Cookie mit __Secure- darf auch eine andere Subdomain derselben Domain setzen, etwa eine Firmenwebsite neben der Instanz, und mit dem Token einer eigenen Sitzung davor gesetzt las die Anmeldung dieses statt des eigenen. Ein Cookie mit __Host- behält der Browser nur für genau den Host, der es gesetzt hat.
  • Ein Beleg wird beim Ausstellen, beim Stornieren und beim Schreiben seiner Positionen gesperrt. Das Ausstellen liest den Beleg mit einer Zeilensperre, bevor es festhält, was in den Schnappschuss kommt, und die Vorgänger, die es abzieht, mit einer geteilten Sperre; das Stornieren sperrt die Rechnung vor seinen Prüfungen; die Routen der Positionen sperren den Entwurf wie der Abgleich seit #582, und der Trigger an den Positionen wartet mit der Migration 0089_line_waits_for_its_document auf eine Transaktion, die den Beleg gerade ausstellt oder unterschreibt. Warum: Eine Position, die im selben Moment geschrieben wurde, kam in den Beleg, aber nicht in seinen Schnappschuss, und ein Storno konnte eine Rechnung treffen, auf die gerade eine Zahlung oder eine abziehende Folgerechnung kam. Beides widerspricht der Festschreibung, die die GoBD verlangen.
  • Ein Anhang, den das Gerät hält, öffnet nur noch als Bild oder PDF in einem neuen Fenster; jede andere Datei wird unter ihrem Namen gespeichert, wie der Server sie ausgibt (shownInPlace, jetzt im Fundament der Domäne). Eine Vorschau, die kein Bild ist, wird weder behalten noch gezeigt. Warum: Geöffnet wurde jede Datei vom Gerät als blob-Seite im Ursprung der Anwendung, unter dem Typ, den ein Browser beim Hochladen angab, eine XML-Datei eingeschlossen, und über eine Version, die eine andere Datei des Mandanten als Vorschau nennt, kam so eine auch auf die Geräte anderer.
  • Die Leser für Tabellen im Fundament gehen ihre Eingabe einmal durch: CSV wird Wert für Wert gelesen (readCsv), sodass die Grenzen beim Lesen gelten; Formate einer Arbeitsmappe, ihre Attribute und Deklarationen werden ohne Muster gelesen, die von jeder Stelle aus neu probierten; ein geteilter Text wird einmal zum Text einer Zelle statt für jede Zelle, die auf ihn zeigt. Der CSV-Export setzt auch Werte mit Komma oder Tabulator in Anführungszeichen. Warum: Eine hochgeladene Tabelle konnte den gemeinsamen Prozess mit dem Quadrat ihrer Größe beschäftigen oder ein Vielfaches ihrer Größe in den Speicher legen, und ein Programm, das das Komma oder den Tabulator für das Trennzeichen hält, begann mitten in einem exportierten Wert eine Zelle, vor der kein Apostroph mehr stand.
  • Die Anmeldung reicht nur noch Pfade an better-auth weiter, für die es eine Route gibt; jeder andere unter /api/auth bekommt vorher eine 404, auch die beiden Routen mit einem Wert im Pfad, die hier niemand braucht. Die Route für das Logo liest ihren Body selbst, nach den Guards und ohne ihn zu entpacken, statt eines Parsers davor. Warum: Die Ratengrenzen von better-auth legten für jeden ausgedachten Pfad eine Zeile in der Datenbank an, von jedem und ohne Sitzung, und der Parser vor dem Logo las und entpackte den Body jeder Anfrage mit einem Bildtyp, bevor gefragt war, wer sie schickt.
  • Die Sperre nach zehn falschen Codes bei der Anmeldung zählt jeden Versuch, bevor der Code geprüft wird, und der zehnte setzt sie im selben Schritt; das gilt für den Code aus der App und für Wiederherstellungscodes. Die Sperre von better-auth ist dafür abgeschaltet. Warum: better-auth sah vor dem Code nach, ob das Konto gesperrt ist, und zählte erst danach, sodass gleichzeitig geschickte Versuche alle bis zum Code kamen; im Test waren es 25 von 25 statt 10.
  • Drei Wege gehen in einem Durchgang statt mit dem Quadrat ihrer Eingabe: der DATANORM-Leser findet die gefüllten Zeilen ohne ein Muster, das eine lange Zeile aus Leerzeichen von jeder Stelle aus neu probierte; der Stromkreisplan gruppiert Abschnitte und Stromkreise einmal, statt sie für jeden Verteiler und Abschnitt neu zu filtern; der Abruf eines Geräts fragt je Zeile über einen Index, ob die Person sie angelegt hat, statt die Liste all ihrer Einträge zu durchsuchen. Warum: Alle drei laufen im gemeinsamen Prozess oder auf dem gemeinsamen Pool, und eine Eingabe eines Mandanten hielt so die Anfragen aller anderen über Sekunden bis Minuten auf.
  • Das Installationspaket ist ab dieser Fassung signiert wie die Abbilder, ohne Schlüssel über Sigstore, und die Release-Seite trägt die Datei opengewerk-<fassung>.tar.gz.sigstore.json und den Befehl cosign verify-blob dazu. Warum: Das Paket legt fest, welche Abbilder der Start prüft und gegen welchen Workflow, und seine Skripte laufen mit den Rechten von Docker; die Prüfsumme daneben stammt von derselben Seite und sagt nur, dass beides zusammen hochgeladen wurde.
  • Fristen und Grenzen im Fundament: der Pool der Datenbank wartet höchstens 30 Sekunden auf eine Verbindung, eine Anweisung hat zwei Minuten und eine untätige Transaktion fünf; eine Mail hat zwei Minuten bis zur Annahme; der Name eines Push-Abos drei Sekunden zum Auflösen; eine Person höchstens 20 Geräte für Push; GET /sync/conflicts liefert die ältesten 200 offenen Konflikte. Warum: Ohne sie konnte eine Anfrage oder ein Lauf eines Betriebs eine Verbindung, den Versand der Mails oder die Auflösung von Namen für alle anderen ohne Ende festhalten.
  • Drucke warten im Prozess auf den Renderer: einer zur Zeit, wie der Renderer sie nimmt, höchstens zehn wartend, davon drei für einen Mandanten, keiner länger als eine Minute (queuedRenderer); darüber hinaus antwortet die Route mit 503 und dem Satz, dass der Renderer gerade für andere druckt. Warum: Jede Route, die druckt, rendert bei jedem Aufruf neu, eine Instanz hat einen Renderer, und jeder Auftrag ging sofort an ihn. Wer einen Druck öffnen durfte, hielt ihn so für alle Mandanten besetzt, und die Aufträge stauten sich dahinter.
  • Ablehnungen von Mailserver und Push-Dienst verraten nicht mehr, was der Resolver der Instanz geantwortet hat: ein Name, der nach innen zeigt, wird wie ein Name abgelehnt, der sich nicht auflösen lässt, beim Mailserver mit demselben Satz und derselben Art von Fehler (unreachableName, EDNS), beim Push mit endpointNameRefused, und keine Ablehnung nennt mehr die interne Adresse. Eine interne Adresse, die jemand selbst einträgt, wird weiter als solche benannt. Warum: Wer einen Mailserver einträgt oder ein Gerät für Push anmeldet, konnte so Name für Name erfragen, welche Namen und Adressen im Netz der Instanz liegen; beim Push genügte dafür jede Rolle.
  • Ein Beleg hat Grenzen: die Bezeichnung einer Position höchstens 1000 Zeichen, ihre Beschreibung 30.000, der Betreff 500, die Texte vor und nach den Positionen je 30.000, und ein Beleg höchstens 2000 Positionen. Routen und Abgleich lehnen mit einem Satz ab, die Migration 0091_document_bounds hält dasselbe mit Checks und dem Trigger document_lines_within_count (OG007); vorhandene Belege bleiben, wie sie sind. Warum: Wer Belege schreiben darf, konnte die gemeinsame Datenbank mit Positionen beliebiger Länge und Zahl füllen, und das Prüfprotokoll hält jede davon für immer.
  • Abzüge und PDF eines Schlussrechnungsentwurfs gibt es nur noch mit dem Recht, Zahlungen einzusehen; ohne es antwortet der Server mit 403 und einem Satz. Warum: Der Entwurf rechnet mit dem, was auf die Abschlagsrechnungen eingegangen ist, und so sah ein Monteur Zahlungen, die ihm der Rechtekatalog ausdrücklich nicht zeigt.
  • Ein Text, den der Abgleich in einer Operation bringt, ist höchstens 100.000 Zeichen lang, so lang, wie eine Route ihn annimmt; die Werte eines Formulars dürfen bis zu ihrer Prüfung reichen. Dazu zählt jedes Konto, was es in einer Stunde als JSON ändert, über alle Mandanten zusammen, und über 100 MB antwortet jede Route mit 429. Warum: Über den Abgleich kam in ein einzelnes Feld ein Text von fast 8 MB, und wie oft jemand schrieb, hatte keine Grenze; ein Mitglied konnte so die Datenbank füllen, die alle Mandanten teilen.
  • Eine E-Rechnung über 10 MB wird nicht mehr geschrieben, sondern mit einem Satz abgelehnt (422), bevor ihr XML in den Parser kommt (largestCiiBytes). Warum: Die Schemaprüfung kopiert jede E-Rechnung in den einen Parser des Prozesses und baut einen Baum daraus, und ein Beleg mit langen Positionstexten, jedes & als &amp; geschrieben, kam auch innerhalb der neuen Grenzen auf Hunderte Megabyte; der Prozess stand dann für jeden Mandanten.
  • Ein Monteur nimmt über den Abgleich keinen stornierten Auftrag mehr wieder auf, und eine Unterschrift landet nur noch unter einem Regiebericht im Entwurf, der an keinem anderen Beleg hängt. Die Prüfung jobProgress fragt den gespeicherten Status, signatureRefusal die Art und den Vorgänger, und die Migration 0090_sign_only_a_report lässt sign_document nur noch einen solchen Bericht festschreiben. Mail und Datei geben einen unterschriebenen Beleg ohne Nummer nur noch als Bericht heraus. Warum: Das Recht für eine Statusänderung hing nur am neuen Status, und eine Unterschrift unter einer Rechnung, einem Angebot oder einer Auftragsbestätigung schrieb den Entwurf mit document.write fest, an document.issue vorbei.
  • Das Änderungsprotokoll schreibt jeden neuen Eintrag in Fassung 3: jeder Wert bekommt 32 zufällige Bytes in audit_value_nonces, sein Fingerabdruck geht über diese und den Wert, und das Schwärzen löscht sie mit dem Wert (Migration 0092, Nachtrag zu ADR 0013). Warum: Der Fingerabdruck war SHA-256 des Werts allein und blieb beim Schwärzen stehen, sodass ein Name oder ein Datum aus Datenbank und Sicherung zu erraten war. Einträge, die schon geschrieben sind, behalten ihren Fingerabdruck, weil kein Eintrag der Kette neu geschrieben wird.
  • Der Lauf der Fristen erinnert je Mandant und Durchgang an höchstens 100 Fristen, die früheste zuerst; die übrigen kommen im nächsten Durchgang eine Minute später (mostRemindersPerPass), der hinter der zuletzt versuchten Frist weitermacht, damit Fristen, deren Erinnerung jedes Mal scheitert, die späteren nicht aufhalten. Warum: Ein Durchgang geht die Mandanten nacheinander durch, und ein geändertes Intervall macht jede offene Frist einer Art auf einmal fällig, jede mit einer eigenen Transaktion und einer Aufgabe. Ohne Grenze hielt ein Mandant damit die Erinnerungen aller anderen auf.
  • Die stündlichen Grenzen für Uploads zählen ein Konto für sich, in welchem Mandanten es auch ablegt, und dazu die ganze Instanz (instanceBytesPerHour, 20 GB). Ein DATANORM-Import hat ein Budget von 400 MB für alle seine Dateien und Archive zusammen statt eines je Archiv, nimmt jede Datei nur einmal an, und Importe laufen im Prozess nacheinander statt einer je Mandant zugleich. Warum: Ein Mandant ist in einer Minute angelegt, und Grenzen je Konto im Mandanten, je Archiv und je Mandant vervielfachten sich so mit der Zahl der Mandanten und Dateien, die eine Person öffnet oder nennt.
  • Belegdateien, die der Server selbst erzeugt (PDF, XRechnung, ZUGFeRD), lassen das letzte Gigabyte auf dem Speicher frei (lastFreeBytes) und zählen je Mandant höchstens 500 MB in der Stunde (tenantMadeBytesPerHour); darüber antwortet der Beleg mit 507 oder 429, und die Mail mit dem Beleg versucht es später wieder. Von zwei gleichzeitigen ersten Anfragen nimmt die, die den Beleg nicht verknüpft, ihre Zeile in files zurück. Warum: Drucke fragten keine Grenze, und die Drucke eines Mandanten konnten den Speicher bis zum letzten Byte füllen, auf dem auch Datenbank und Sicherung liegen.
  • nodemailer 10.2.1 statt 10.0.10 im Server, mit der Sperrdatei (Dependabot-Alerts 3 bis 7). Die älteren Fassungen zerlegten eine Adresse mit Kommentar, Anführungszeichen oder spitzer Klammer an der falschen Stelle, sodass aus Anzeigetext ein Empfänger werden konnte, und erkannten die Anmeldeverfahren eines Mailservers mit einem Ausdruck, der bei einer langen Antwort quadratisch lief. 10.3.1 ist erst seit heute erschienen und noch nicht alt genug für die Wartezeit der Sperrdatei. Dazu happy-dom 21.0.4 in beiden Oberflächen (Alerts 8 bis 13); das läuft nur in den Tests, nicht im gebauten Paket.

Installieren und aktualisieren

Das Paket opengewerk-0.6.3.tar.gz enthält den Ordner opengewerk/docker. Zum Installieren entpacken, mit tar --no-same-owner -xzf opengewerk-0.6.3.tar.gz, und im Ordner opengewerk sh docker/start.sh aufrufen. Für ein Update das neue Paket genauso an dieselbe Stelle entpacken, die .env bleibt, und derselbe Befehl. Gebaut wird dabei nichts, geholt werden die Abbilder dieser Fassung, für x86_64 und ARM64:

  • ghcr.io/opengewerk/opengewerk:0.6.3 (sha256:6c427223033fbb86f42374e6bdec13115723ab38681154e2f6cddb6d1a3f3579)
  • ghcr.io/opengewerk/backup:0.6.3 (sha256:4c4a31744c2fa573ccd97890a477c801edf0685690973ab8442fc140d4f32e28)

Dieselben Abbilder liegen mit denselben Digests und Signaturen auch auf Docker Hub, als opengewerk/opengewerk:0.6.3 und opengewerk/backup:0.6.3. Das Paket holt sie trotzdem aus ghcr.io, denn Docker Hub begrenzt anonyme Pulls.

Gebraucht werden Docker und Docker Compose ab 2.22. Beide Abbilder sind ohne Schlüssel über Sigstore signiert. Prüfen:

cosign verify ghcr.io/opengewerk/opengewerk:0.6.3 \
  --certificate-identity https://github.com/opengewerk/opengewerk/.github/workflows/release.yml@refs/tags/v0.6.3 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Das Paket ist im selben Lauf genauso signiert. Vor dem Entpacken prüfen, mit opengewerk-0.6.3.tar.gz.sigstore.json daneben:

cosign verify-blob opengewerk-0.6.3.tar.gz \
  --bundle opengewerk-0.6.3.tar.gz.sigstore.json \
  --certificate-identity https://github.com/opengewerk/opengewerk/.github/workflows/release.yml@refs/tags/v0.6.3 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com