--- type: issue id: "0099" status: done created: 2026-08-06 milestone: M1 priority: medium projekt: threadnet-web gitlab_iid: "12" related: [] --- # Upstream-Sicherheitsfixes lassen sich nicht mergen — kein gemeinsamer Vorfahre > Adoptiert aus [threadnet-web#12](https://git.lab/axion1337.chat/ThreadNet-Web/-/issues/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](../adr/0021-foederation-geschlossen.md) 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 2–4 (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 2–4 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/develop` → **leer** | | 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](../adr/0022-upstream-anschluss-durch-einmaligen-merge.md)). ### `--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 1–2 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.