Files
management/docs/issues/0099-threadnet-web-12-upstream-sicherheitsfixes-lassen-sich-nicht-m.md
T
Thore Cimbal 6652125ed3 #0099 geschlossen: Upstream-Anschluss vollzogen, v0.6.0 in Produktion
ThreadNet-Web:main auf 8ca03fe, Merge-Commit 88c4e15 mit beiden echten Eltern.
GHSA-wrcp-5v3v-3j6v ist mit dem Versionssprung erledigt.

Drei Anlaeufe: rc.1 erzeugte kein Image (.npmrc fehlte im Docker-Kontext, unter
pnpm 11 fatal), rc.2 ging live und brach die Raumliste, rc.3 bestand die Abnahme.

ADR-0022 um zwei Korrekturen ergaenzt, die erst die Ausfuehrung gezeigt hat:

Die stille Klasse verschwindet nicht ganz, sie dreht sich um. Der Merge meldet,
wenn Upstream eine Datei verschiebt - das hat gehalten. Er meldet nicht, wenn
beide Seiten die Aufloesung ueberleben und nur eine noch Sinn ergibt. Genau das
brach rc.2.

Und: ein gruener Build war nie eine Abnahme. Der web-Job baut nur, webpack
wirft Typen weg. tsc meldete den Fehler durchgehend, gefragt hatte ihn niemand.
Seit 8ca03fe fuehrt docker_web den Job typecheck als needs.

Daraus die stehende Regel fuer kuenftige Merges: nach der Konfliktaufloesung auf
ueberlebende Reste pruefen, nicht nur auf verlorene Zeilen. Wo Zeichenketten
statt Typen im Spiel sind, braucht es einen Abgleich gegen die Registry - fuer
Einstellungen einmalig gefahren, 135 abgefragte gegen 152 registrierte.
2026-08-19 12:00:00 +00:00

20 KiB
Raw Blame History

type, id, status, created, milestone, priority, projekt, gitlab_iid, related
type id status created milestone priority projekt gitlab_iid related
issue 0099 done 2026-08-06 M1 medium threadnet-web 12

Upstream-Sicherheitsfixes lassen sich nicht mergen — kein gemeinsamer Vorfahre

Adoptiert aus threadnet-web#12 (2026-08-18, ADR-0019). Kommentare und Verlauf bleiben dort; kanonisch ist ab jetzt diese Datei.

Am 2026-08-06 gemessen (docs/axion1337-fork.md, Abschnitt 4): Dieses Repo hat keine Upstream-Historie. Element Web 1.12.17 kam am 2026-05-10 als kompletter Baum herein — in 3da3635, im selben Commit wie das erste eigene Feature.

Damit gibt es keinen gemeinsamen Vorfahren mit element-hq/element-web. git merge upstream/develop ist nicht möglich; erzwungen kollidiert praktisch jede Datei.

Warum das ein Sicherheitsthema ist, kein Build-Thema

Element veröffentlicht Sicherheitsfixes als neue Version. Bei einem normalen Fork zieht man sie mit einem Merge. Bei uns bedeutet dasselbe: neuen Upstream-Stand beschaffen und unsere 12 Patches von Hand neu auftragen — davon sieben in der Medien-Pipeline, wo Element gerade auf MVVM umbaut.

Das ergibt eine unangenehme Kette mit gitops#22 (Advisory-Monitoring): Wir würden von einer Lücke erfahren und wären trotzdem langsam. Die Zeitspanne zwischen „bekannt" und „gepatcht" ist das, was zählt — und sie ist hier strukturell zu lang.

⚠️ Verschärfend: Verschiebt Element beim MVVM-Umbau eine der viewmodels/-Dateien, entsteht kein Konflikt — unsere Zeilen sind schlicht weg, und Git meldet nichts.

Was zu tun ist

  1. Zuerst messen, nicht bauen: Auf welchem Stand ist Upstream inzwischen, und sind seit 1.12.17 Sicherheitsfixes für Element Web erschienen? Das beantwortet, ob das dringend ist oder Vorsorge.
  2. element-hq/element-web als zweiten Remote aufnehmen und den Tag von 1.12.17 holen. Damit lässt sich ein Update wenigstens als Diff betrachten, statt blind zu kopieren.
  3. Prüfen, ob sich ein gemeinsamer Vorfahre nachträglich herstellen lässt — ein Graft/Replace des Import-Commits auf den passenden Upstream-Tag. Wenn das trägt, sind künftige Updates wieder ein Merge.
  4. Falls nicht: einen Ablauf schreiben, wie unsere 12 Patches auf einen neuen Stand aufgetragen werden — mit dem Funktionstest für ClamAV als Abnahme (verschlüsselte Datei senden, abgelehnte empfangen).

Zusammenhang: gitops#22 (Advisory-Monitoring) ist die Erkennung, dieses Issue die Reaktionsfähigkeit. Das eine nützt wenig ohne das andere.

Gefunden am 2026-08-06 beim Vermessen der Merge-Reibung (Arbeitspaket 3 aus #7).

Schritt 1 beantwortet 2026-08-19 — es ist keine Vorsorge mehr

Das Issue verlangt ausdrücklich „zuerst messen, nicht bauen". Gemessen:

Wir sind neun Veröffentlichungen zurück. Import war Element Web 1.12.17 (2026-05-10); Upstream steht bei v1.12.26 (2026-08-18).

Und es gibt eine offene Sicherheitsmeldung, die uns betrifft:

Advisory GHSA-wrcp-5v3v-3j6v
veröffentlicht 2026-07-20 (vier Wochen offen)
Einstufung medium
Inhalt Ein bösartiger Homeserver kann über die Startseite HTML in Element Web einschleusen
betroffen < v1.12.22 — wir laufen 1.12.17

Die übrigen fünf Advisories im Projekt betreffen ältere Stände (≤ 1.11.x) und damit nicht uns.

Einordnung — real, aber schmal

Der Angriff setzt voraus, dass der Client auf einen bösartigen Homeserver zeigt. Unsere Nutzer verwenden ihn gegen den eigenen Server, und Föderation ist seit ADR-0021 geschlossen.

Aber die Voraussetzung ist erreichbar: disable_custom_urls ist in element-values.yaml nicht gesetzt, Element erlaubt damit standardmäßig die Eingabe eines beliebigen Homeservers — und die Desktop-Builds werden verteilt. Wer sich mit unserem Client woanders anmeldet, ist ungeschützt.

Was daraus folgt

Die Antwort auf „dringend oder Vorsorge" lautet: weder Panik noch Vorsorge, sondern überfällig. Ein Fork ohne Merge-Weg hat vier Wochen gebraucht, um eine bekannte Lücke nicht zu schließen — genau die Zeitspanne, die dieses Issue als das eigentliche Problem benennt.

Sofort möglich, unabhängig vom Merge-Problem: disable_custom_urls: true setzen. Das nimmt dem Advisory für unsere Nutzer die Voraussetzung, kostet eine Zeile und berührt die Fork-Frage nicht. Ob fremde Homeserver überhaupt erlaubt sein sollen, ist ohnehin eine eigene Entscheidung — bei geschlossener Föderation liegt „nein" nahe.

Schritte 24 (Upstream als Remote, Graft-Versuch, Patch-Auftragsverfahren) bleiben offen; sie sind der eigentliche Umfang dieses Issues.

Teilmaßnahme 2026-08-19 — disable_custom_urls an beiden Stellen

Entscheidung sorb. Gesetzt in zwei Dateien, nicht in einer:

Ort Datei Commit wirksam
Web-Client gitops:apps/production/custom-configs/element-values.yaml d809b3f mit dem Flux-Abgleich (Element liest config.json beim Laden)
Desktop-Client ThreadNet-Web:apps/desktop/axion1337/config.json e1e9a19 erst mit dem nächsten Desktop-Build

Die zweite Stelle wäre fast durchgerutscht: Der Desktop-Client lädt seine eigene config.json, nicht die des Web-Deployments — derselbe Fallstrick wie beim Themes-Rollout. Eine Änderung nur in element-values.yaml hätte den Web-Client abgesichert und genau die Builds unberührt gelassen, die verteilt werden.

Was sie bewirkt — und was nicht

Im Code unserer Fassung nachgelesen, nicht angenommen:

  • ServerPicker.tsx: Der „Bearbeiten"-Knopf am Servernamen wird nicht gerendert. Über die Oberfläche ist der Homeserver damit nicht mehr wechselbar.
  • ErrorUtils.tsx: Bei 401/403 benennt die Fehlermeldung den Server, statt generisch zu bleiben — kleiner Nebengewinn für den Alltag.

⚠️ Es ist eine Oberflächen-Sperre, keine technische. MatrixChat.tsx übernimmt hs_url weiterhin aus der URL — in zwei Registrierungs-Flüssen (mobile Registrierung, Bestätigungslink) und ohne disable_custom_urls zu prüfen. Ein präparierter Link bleibt davon unberührt.

Für GHSA-wrcp-5v3v-3j6v heißt das: Der bequeme, sichtbare Weg zu einem fremden Homeserver ist weg, alle Wege sind es nicht. Die Maßnahme senkt die Wahrscheinlichkeit, dass jemand versehentlich oder auf Zuruf woanders landet — sie ist kein Ersatz für das Upstream-Update. Das bleibt Schritt 24 dieses Issues.

Schritt 2 erledigt 2026-08-19 — und er widerlegt eine Annahme des Issues

upstream (element-hq/element-web) ist als zweiter Remote eingerichtet, v1.12.17 und v1.12.26 sind flach geholt. Der Remote ist lokale Konfiguration und wird nicht mitcommittet; die Einrichtung steht jetzt in ThreadNet-Web:docs/axion1337-fork.md (Commit fa5dcc5).

Der Import ist NICHT der Tag v1.12.17

Das Issue und die Fork-Doku behaupteten beide, „Element Web 1.12.17 kam am 2026-05-10 als kompletter Baum herein". Die erste Messung mit dem neuen Remote widerspricht dem:

Messung Ergebnis
git diff --stat v1.12.17 3da3635 783 Dateien, +17443 / 10371
nur bei uns vorhanden 115 Dateien
nur im Tag vorhanden 62 Dateien

Entscheidend sind zwei davon, weil sie datieren: apps/web/src/viewmodels/room/timeline/event-tile/body/TextualBodyViewModel.tsx und packages/shared-components/src/core/roving/RovingTabIndex.tsx — im Tag v1.12.17 nicht vorhanden, in v1.12.26 und in unserem Import vorhanden. Der CHANGELOG.md unseres Imports endet bei 1.12.17 (2026-04-30), weil er erst beim Release fortgeschrieben wird.

Die Grundlage ist also ein develop-Stand nach dem 1.12.17-Release (1.12.18 kam am 2026-05-12), nicht der Tag. Ausgerechnet MVVM-Dateien belegen es — der Bereich, vor dem dieses Issue ohnehin warnt.

Folge für Schritt 3

Ein Graft auf v1.12.17 wäre falsch. Er würde jede künftige Zusammenführung auf eine erfundene Ahnenreihe stellen und Konflikte an Stellen erzeugen, wo keine sind — oder schlimmer: keine erzeugen, wo welche gehören. Wer den gemeinsamen Vorfahren nachträglich herstellen will, muss den passenden develop-Commit finden. Das braucht die volle Upstream-Historie; ein flacher Tag-Fetch reicht dafür nicht.

Schritt 3 bleibt damit offen, aber er hat jetzt eine tragfähige Fragestellung statt einer falschen Zielmarke.

Unverändert: die Advisory-Bewertung

Der Basisstand liegt zwischen 1.12.17 und 1.12.18 und damit klar unter 1.12.22 — GHSA-wrcp-5v3v-3j6v trifft uns weiterhin. Die Korrektur ändert die Dringlichkeit nicht, nur den Weg dorthin.

Schritt 3 beantwortet 2026-08-19 — der Graft trägt, und er halbiert das Problem

Volle Upstream-Historie geholt (70.330 Commits, .git wächst von 87 MB auf 603 MB). Damit ließ sich der gesuchte gemeinsame Vorfahre suchen statt raten.

Die Basis ist deadd548 vom 2026-05-08

Verfahren: für jeden develop-Commit im Fenster 25.04.16.05. die Zahl der gegen unseren Import abweichenden Dateien messen — der Basis-Commit ist das Minimum. Gemessen wurde der Baum, nicht das Datum.

Ergebnis: deadd548 („Update storybook", #33437) mit 43 abweichenden Dateien — gegenüber 783 beim fälschlich angenommenen Tag v1.12.17. Und diese 43 zerfallen sauber:

Art Anzahl Was es ist
nur Modus (755 → 644) 31 der Import-Bug, den die Fork-Doku als historisch beschreibt
echte Inhaltsänderung 12 davon 5 + 1 Test = unser Discord-Feature, dazu LICENSE und CHANGELOG.md
Hinzufügungen 2 unsere Feature-Dateien

Der inhaltliche Rest ist also genau das, was wir damals selbst geändert haben. Damit ist deadd548 der Stand, aus dem der Fork genommen wurde.

Der Graft wurde getestet, nicht behauptet

In einem Wegwerf-Klon (Produktions-Repo unberührt, danach entfernt):

vor dem Graft git merge-base main upstream/developleer
nach git replace --graft 3da3635 deadd548 gemeinsamer Vorfahre deadd548
git merge upstream/develop läuft durch, 32 konfliktbehaftete Dateien

⚠️ Ein Zwischenergebnis war wertlos und wäre fast als Erfolg durchgegangen: Der erste Merge-Versuch gegen v1.12.26 meldete „0 Konflikte" — er war aber gar nicht gelaufen („refusing to merge unrelated histories"), weil der Tag flach geholt war und nur einen einzigen Commit trägt. Erst die Gegenprobe auf MERGE_HEAD deckte das auf.

Was die 32 Konflikte sind

Nur vier sind Quellcode — und es sind genau unsere Patches: MImageBody.tsx, MAudioBody.tsx (ClamAV), SdkConfig.ts (Branding), RoomListItemViewModel.ts (Discord-Feature). Der Rest sind .github/workflows/* (die Fork-Doku führt sie als „gering — eigene Strecke") und pnpm-lock.yaml, das ohnehin regeneriert wird.

Was das ändert

Aus „neuen Upstream-Baum beschaffen und zwölf Patches von Hand neu auftragen" wird „vier Quellcode-Konflikte in einem normalen Drei-Wege-Merge lösen".

Wichtiger noch ist die stille Gefahr, die verschwindet. Das Issue warnt: „Verschiebt Element beim MVVM-Umbau eine der viewmodels/-Dateien, entsteht kein Konflikt — unsere Zeilen sind schlicht weg, und Git meldet nichts." Mit echtem gemeinsamem Vorfahren meldet Git es. Das ist der eigentliche Gewinn, nicht die Zahl.

Entscheidung, die noch aussteht

Der Graft ist eine lokale Ersetzung (refs/replace/*). Damit alle davon haben, müsste er mitgeliefert werden — git push origin 'refs/replace/*' und ein Hinweis in der Doku, dass Klone git fetch origin 'refs/replace/*:refs/replace/*' brauchen. Alternative: kein Graft, sondern beim nächsten Update einmalig einen echten Merge-Commit mit --allow-unrelated-histories bauen und ab dann normal weitermergen.

Beides ändert die Historie-Wahrnehmung des Repos dauerhaft und ist damit ADR-pflichtig. Entscheidung sorb steht aus; Schritt 4 (Auftragsverfahren für die Patches) wird bei Weg 1 oder 2 weitgehend gegenstandslos.

Weg B belegt 2026-08-19 — mit einer wichtigen Präzisierung

Entscheidung sorb: Weg B (ADR-0022).

--allow-unrelated-histories allein genügt NICHT

Gemessen gegen v1.12.26, im selben Klon, nacheinander:

Vorgehen Konflikte
nur --allow-unrelated-histories, ohne Vorfahren 1757
mit lokal gesetztem Graft auf deadd548 32

Ohne gemeinsamen Vorfahren macht Git einen Zwei-Wege-Vergleich — das ist die „praktisch jede Datei konfliktet"-Lage aus der Fork-Doku. Der Graft ist also kein Gegenentwurf zu Weg B, sondern sein Werkzeug: lokal setzen, den Merge damit rechnen lassen, committen — der Merge-Commit trägt danach die echten Eltern, und der Graft kann weg. Ab dann läuft die Abstammung über den Merge-Commit selbst. Das steht so nicht in ADR-0022s Optionsbeschreibung und ist hier nachgetragen.

⚠️ Stolperstein auf dem Weg dahin: Die mit --depth=1 geholten Tags tragen eine .git/shallow-Grenze. v1.12.26 war dadurch auf einen Commit abgemauert, obwohl derselbe Commit über develop vollständig vorlag — jeder Merge-Versuch scheiterte mit „refusing to merge unrelated histories", was wie ein Grundproblem aussah und keines war. git fetch --unshallow upstream löst es.

Die 32 Konflikte, vollständig aufgeschlüsselt

Menge Was Auflösung
24 .github/workflows/* unsere Fassung behalten — eigene CI-Strecke
1 pnpm-lock.yaml regenerieren
1 apps/web/package.json Produktentscheidung, siehe unten
6 Quellcode, je 12 Konfliktblöcke von Hand

Der Fund, der die ganze Übung rechtfertigt: Upstream hat RoomListItemAccessibilityWrapper in RoomListItemWrapper umbenannt. Weil es jetzt einen echten Vorfahren gibt, meldet Git den Konflikt — genau der Fall, den dieses Issue als stille Gefahr beschreibt („unsere Zeilen sind schlicht weg, und Git meldet nichts").

Warum ich hier gestoppt habe

apps/web/package.json enthält zwei Zeilen, die keine Konfliktauflösung sind, sondern Entscheidungen:

ours:     "@sorb/threadnet-call-embedded": "0.19.2-threadnet.12"
upstream: "@element-hq/element-call-embedded": "0.22.0"

ours:     "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#d19cb751..."
upstream: "matrix-js-sdk": "42.2.0"

Die erste falsch aufgelöst, und die gesamte KI-Geräuschunterdrückung (#0054) ist stillschweigend weg. Die zweite betrifft den Git-Ref-Pin, der seinerzeit ein Build-Fix war (Abschnitt 3 der Fork-Doku) — ob Upstreams 42.2.0 ihn erübrigt, zeigt nur ein Build.

Beides ist ohne Build und ohne den ClamAV-Funktionstest nicht absicherbar. Der Merge ist damit vorbereitet und vermessen, aber nicht ausgeführt; das Experiment wurde abgeräumt (kein Zweig, kein Replace-Ref, Arbeitsverzeichnis sauber). Die Upstream-Historie bleibt im Klon liegen, damit der nächste Anlauf nicht wieder 600 MB holen muss.

Nächster Schritt: den Merge als eigenes Vorhaben fahren, mit Build und ClamAV-Abnahme als Voraussetzung — nicht als Nebenschritt.

Ausgeführt und abgenommen 2026-08-19 — v0.6.0 läuft in Produktion

Der Merge ist vollzogen. ThreadNet-Web:main steht auf 8ca03fe; der Merge-Commit 88c4e15 trägt beide echten Eltern (fa5dcc5 = unser bisheriger main, c43ef70b = v1.12.26). Damit ist die Abstammung hergestellt, der Graft ist weg, und GHSA-wrcp-5v3v-3j6v ist mit dem Versionssprung erledigt.

Die beiden Produktentscheidungen aus dem Abschnitt davor

Zeile Auflösung Beleg
@sorb/threadnet-call-embedded unsere behalten, 0.19.2-threadnet.12 auf main und im Zweig identisch — die KI-Geräuschunterdrückung (#0054) ist unberührt
matrix-js-sdk Upstreams 42.2.0 genommen, Git-Ref-Pin fällt weg Build läuft; der seinerzeitige Build-Fix hat sich erübrigt

Drei Anläufe, zwei davon gescheitert

v0.6.0-rc.1 kein Image. docker_web scheiterte: .npmrc stand nie in der COPY-Zeile des Dockerfiles. Unter pnpm 10 folgenlos, weil --frozen-lockfile die gepinnte Tarball-URL nahm; pnpm 11 prüft mit minimumReleaseAgeStrict das Alter jedes Eintrags, löst den Scope wieder auf und landet ohne .npmrc bei npmjs → 404. Genau der Ablauf, den die .npmrc selbst vorhersagt (#0055).
v0.6.0-rc.2 ging live und brach die Raumliste. Nach wenigen Minuten auf v0.5.4 zurückgenommen. Siehe unten.
v0.6.0-rc.3 Abnahme bestanden, als v0.6.0 freigegeben.

Der rc.2-Vorfall — eine stille Leiche der anderen Sorte

RoomListItemViewModel.ts rief SettingsStore.getValue("feature_room_list_sections") auf einen Labs-Schalter, den Upstream entfernt hat; Sektionen laufen dort über RoomList.showSections. Die Auflösung hatte überall Upstreams Seite genommen — Menü, View, Snapshot, Typen — und nur diese eine const-Zeile aus unserer Seite stehen lassen. Sie wurde tree-weit von niemandem gelesen und warf trotzdem: bei jedem Raumlisteneintrag, als react-soft-crash.

Bemerkenswert ist die Richtung: Dieses Issue warnt vor Zeilen, die verschwinden. Hier ist eine Zeile übrig geblieben, die verschwinden musste. Der Merge meldet Konflikte, wo Dateien wandern — er merkt aber nicht, wenn beide Seiten überleben und nur eine davon noch Sinn ergibt.

Abgesichert statt gehofft: alle 135 im Quellbaum abgefragten Einstellungen gegen die 152 in Settings.tsx registrierten verglichen — genau diese eine Leiche, keine weitere. Ein zweiter Rest derselben Art (ungenutzter Import ElementDesktopLogoSvg in SdkConfig.ts) war harmlos; geprüft wurde dabei ausdrücklich, ob unser Rebrand gelitten hat — hat er nicht, desktopBuilds trägt weiter eigenes Logo und eigenen Release-Pfad.

Warum kein Build das fangen konnte — und was daraus folgt

Der CI-Job web führt ausschließlich pnpm --dir apps/web build aus. webpack entfernt Typen, ohne sie zu prüfen; ein unbekannter Einstellungsschlüssel ist zur Bauzeit bloß ein String. tsc dagegen meldete den Fehler die ganze Zeit — zweimal (TS2345 unbekannter Schlüssel, TS6133 ungenutzte Konstante). Gefragt hatte ihn niemand.

Konsequenz, umgesetzt in 8ca03fe: neuer Job typecheck, den docker_web als needs führt. Kein Image mehr ohne bestandene Typprüfung. Maßstab ist „kein Fehler außerhalb von node_modules", weil Upstream v1.12.26 selbst nicht typrein ist — matrix-js-sdk@42.2.0 wirft drei Fehler in der eigenen Quelle, in einem sauberen v1.12.26-Checkout gegengeprüft.

⚠️ Das Tor wäre beim Bau selbst fast wertlos geworden: Das erste grep "error TS" hätte nie gegriffen, weil nx auch in der Pipe färbt und zwischen error und TS eine Escape-Sequenz steht. Es ist jetzt in beide Richtungen belegt — mit wieder eingesetzter Zeile scheitert es und benennt beide Fehler, ohne sie besteht es — und scheitert zusätzlich bei leerer tsc-Ausgabe, damit ein stiller Erfolg nicht als Prüfung durchgeht.

Die Abnahme, wie dieses Issue sie verlangt

Gefordert war „verschlüsselte Datei senden, abgelehnte empfangen" — geprüft am laufenden System, nicht am Build:

Prüfpunkt Ergebnis
Raumliste lädt mit konfigurierten Sektionen, also im kritischen Pfad
ClamAV Sendepfad blockiert vor dem Upload — ein Scan-Aufruf, kein zweiter
ClamAV Empfangspfad (Datei) EICAR erkannt, Abweisung wird angezeigt
ClamAV Bildpfad (.png) zugestellt, beim Herunterladen abgewiesen, Meldung gerendert
Call-Teilnehmerliste

Der .png-Fall ist der wichtigste: Er ist der einzige, der den portierten Code ImageBodyViewModel.computeErrorLabel() tatsächlich durchläuft. Wäre die Datei schon beim Senden geblockt worden, hätte der Test den Sendepfad ein zweites Mal geprüft und den Bildpfad gar nicht — beides sieht im Scanner-Log gleich aus, weil die Meldung flagged an upload/download nicht unterscheidet.

Stand

v0.6.0 ist in Produktion (gitops:d87c432), /version liefert 0.6.0. Rückhebel bleibt der Tag-Revert auf v0.5.4.

Alle vier Schritte dieses Issues sind beantwortet; Schritt 4 ist wie in ADR-0022 vorhergesagt gegenstandslos geworden. Das nächste Upstream-Update ist ein gewöhnlicher Merge.