fix(PROJ-11): BUG-20 — stop generating guessable Matrix localparts
toLocalpart() built vorname.nachname from real names, so anyone could construct another org's Matrix ID and DM them directly — completely sidestepping the user_directory.enabled=false mitigation from BUG-2, which only blocks *searching*, not addressing a known ID. Live-exploited during QA round 3 (cross-club DM succeeded). Now generates a random `u<16 hex chars>` localpart with no relation to the member's name. Display name (passed separately to createUser) still shows the real name in room member lists. sync-worker.ts and invitations.ts both used to retry on collision by appending a numeric suffix to the name-derived base; since there's no name relation left, collision retry now just draws a fresh random id. Verified end-to-end on talkhub/matrix: new org creator got @u747fcd631af6db20:matrix.perlbach24.de instead of a name-derived id. Test data cleaned up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
This commit is contained in:
1 parent
121d80e707
commit
f3361d695e
4 files changed
+23
-15
No files matched your search
@@ -43,12 +43,11 @@ async function provisionMatrixUser(
|
||||
lastName: string,
|
||||
memberId: string,
|
||||
): Promise<void> {
|
||||
const base = toLocalpart(firstName, lastName);
|
||||
const password = randomBytes(24).toString("hex");
|
||||
const displayName = `${firstName} ${lastName}`;
|
||||
|
||||
for (let suffix = 0; suffix < 10; suffix++) {
|
||||
const localpart = suffix === 0 ? base : `${base}${suffix + 1}`;
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
const localpart = toLocalpart();
|
||||
try {
|
||||
await createUser(localpart, { password, displayName });
|
||||
const matrixUserId = toMatrixUserId(localpart);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
const baseUrl = process.env.SYNAPSE_BASE_URL!;
|
||||
const adminToken = process.env.SYNAPSE_ADMIN_TOKEN!;
|
||||
const serverName = process.env.SYNAPSE_SERVER_NAME!;
|
||||
@@ -37,14 +39,14 @@ export function toMatrixUserId(localpart: string): string {
|
||||
return `@${localpart}:${serverName}`;
|
||||
}
|
||||
|
||||
export function toLocalpart(firstName: string, lastName: string): string {
|
||||
const normalize = (part: string) =>
|
||||
part
|
||||
.toLowerCase()
|
||||
.normalize("NFKD")
|
||||
.replace(/[̀-ͯ]/g, "")
|
||||
.replace(/[^a-z0-9]/g, "");
|
||||
return `${normalize(firstName)}.${normalize(lastName)}`;
|
||||
// BUG-20: this used to be `vorname.nachname` — guessable from a member's real
|
||||
// name, which let anyone construct another org's Matrix ID and DM them
|
||||
// directly, bypassing the user_directory.enabled=false mitigation (BUG-2)
|
||||
// entirely. The display name (set separately via createUser's displayName
|
||||
// param) still shows the real name in room member lists; the localpart itself
|
||||
// carries no information about who the member is.
|
||||
export function toLocalpart(): string {
|
||||
return `u${randomBytes(8).toString("hex")}`;
|
||||
}
|
||||
|
||||
export interface SynapseUser {
|
||||
|
||||
@@ -41,12 +41,11 @@ const handlers: {
|
||||
return; // already provisioned, idempotent no-op
|
||||
}
|
||||
|
||||
const base = toLocalpart(firstName, lastName);
|
||||
const password = randomUUID().replace(/-/g, "");
|
||||
const displayName = `${firstName} ${lastName}`;
|
||||
|
||||
for (let suffix = 0; suffix < 10; suffix++) {
|
||||
const localpart = suffix === 0 ? base : `${base}${suffix + 1}`;
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
const localpart = toLocalpart();
|
||||
try {
|
||||
await createUser(localpart, { password, displayName });
|
||||
await db
|
||||
|
||||
@@ -640,7 +640,7 @@ Was jetzt blockiert:
|
||||
|
||||
**BUG-11 (High) — teilgemildert, nicht vollständig gelöst.** Start des App-weiten Unread-Clients läuft jetzt über `requestIdleCallback` (Fallback `setTimeout`), statt synchron beim Mount von `TeamsPage` zu feuern — nimmt die SDK-Ladezeit vom kritischen Pfad der Team-Liste. **Löst aber nicht** das eigentliche QA-Argument: Nutzer, die nie einen Chat öffnen, laden die ~830 KB SDK trotzdem, sobald sie die Team-Liste besuchen und mindestens ein Team einen Matrix-Raum hat. Eine vollständige Lösung bräuchte serverseitig aggregierte Ungelesen-Zahlen statt eines Browser-seitigen Matrix-Clients für reine Badge-Anzeige — als offene Architekturentscheidung dokumentiert, nicht stillschweigend als gelöst deklariert.
|
||||
|
||||
**Nicht in diesem Pass behoben:** BUG-20 (Klarnamen-Matrix-IDs, hebelt BUG-2-Mitigation aus — braucht eigene Entscheidung zu Localpart-Generierung, z.B. zufällige statt namensbasierte IDs), BUG-5/6/7/9/10/13/14/15/16/17/21/22.
|
||||
**Nicht in diesem Pass behoben:** BUG-5/6/7/9/10/13/14/15/16/17/21/22.
|
||||
|
||||
**Testdaten aufgeräumt:** Alle Test-Vereine/-User/-Räume aus Runde 3 (QA-Agent) sowie aus der eigenen Verifikation dieses Bugfix-Passes wurden gelöscht/erased (Postgres cascade + Synapse `deactivate?erase=true` + Room-Purge).
|
||||
|
||||
@@ -648,6 +648,14 @@ Empfehlung: erneut `/qa`, mit Fokus auf BUG-18/19-Verifikation im Browser (nicht
|
||||
|
||||
Bilanz: **1 Critical, 2 High neu; 2 High (BUG-11/12) unverändert offen.** Akzeptanzkriterien formal 7 PASS / 1 Vorbehalt / 0 FAIL — praktisch aber für alle per Einladung ins Team geholten Mitglieder wirkungslos, solange BUG-18 besteht.
|
||||
|
||||
## BUG-20-Fix (2026-09-11, separater Pass)
|
||||
|
||||
**Behoben, live verifiziert.** `toLocalpart()` in `backend/src/services/synapse.ts` generierte bisher `vorname.nachname` — erratbar aus dem echten Namen, was die `user_directory.enabled: false`-Mitigation aus BUG-2 praktisch aushebelte (direktes Anschreiben ohne Suche). Jetzt: `toLocalpart()` erzeugt eine kryptographisch zufällige ID (`u` + 16 Hex-Zeichen aus `randomBytes(8)`), ohne jeden Bezug zum Namen. Der Anzeigename (`displayName`, separat an `createUser()` übergeben) zeigt weiterhin den echten Namen in Raum-Mitgliederlisten — nur die Matrix-ID selbst ist jetzt opak.
|
||||
|
||||
Angepasst: `sync-worker.ts` und `invitations.ts` (beide riefen `toLocalpart(firstName, lastName)` mit anschließender Suffix-Schleife bei Kollision auf `base+suffix` — jetzt wird bei Kollision einfach eine neue Zufalls-ID gezogen, da der Namensbezug ohnehin weg ist).
|
||||
|
||||
Live verifiziert: neuer Vereinsersteller bekommt `@u747fcd631af6db20:matrix.perlbach24.de` statt `@vorname.nachname:...` — kein Rückschluss auf den Namen möglich. Testdaten aufgeräumt.
|
||||
|
||||
**Empfohlene Fix-Reihenfolge:** BUG-18 → BUG-19 → BUG-12 → BUG-11 → BUG-20 (Architekturentscheidung: Raum-Erstellung sperren und/oder ein Synapse pro Verein) → BUG-1 (committen) → BUG-13/14 → BUG-5/15 → BUG-6/21 → Rest.
|
||||
|
||||
**Nach den Fixes zwingend nachzuholen:** der bislang in allen drei Runden ausgefallene **Cross-Browser- und Responsive-Test (Chrome/Firefox/Safari; 375/768/1440 px)** inklusive XSS-Gegenprobe im echten Browser-Rendering.
|
||||
Reference in new issue
Block a user