FORBIDDEN_CHARS_RE in sanitizeNextPath bestand aus drei rohen Steuerbytes (NUL, US, DEL) statt Escapes - jeder Editor, Formatierer oder Minifier in der Kette kann solche Bytes stillschweigend verschlucken. Umgeschrieben auf Unicode-Escapes fuer den Bereich U+0000 bis U+001F und U+007F. Zeichenmenge ueber alle 65536 Codepunkte aus U+0000 bis U+FFFF als unveraendert nachgewiesen (54 abgewiesene Codepunkte, Bitmap-SHA-256 3d58108b87e4641e506602cc701a66d11ec19cf20551937fada1826f50ecbe64 vor und nach dem Umbau identisch). Kommentar korrigiert: die vorherige Behauptung, Escapes seien "am Edge" noetig, war falsch - zwischen den beiden Escape-Schreibweisen gibt es zur Laufzeit keinen Unterschied. Quick-Vorgang 260921-i8x, Task 1/3. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
36 KiB
phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
| phase | plan | type | wave | depends_on | autonomous | requirements | files_modified | estimate | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260921-i8x | 01 | execute | 1 | true |
|
|
|
|
Purpose: Der Weiterleitungsschutz nach der Anmeldung haengt heute an drei rohen Steuerbytes in einer Zeichenklasse. Das funktioniert, ist aber gegen jeden Editor, Formatierer, Minifier und Copy-Paste ungeschuetzt — und der Kommentar zwei Zeilen darueber behauptet bereits faelschlich, es seien Escapes. Die restlichen elf Befunde sind zu klaeren, damit die Klassen nicht als "ungeprueft" im Rueckstand stehen bleiben.
Output: Zwoelf abgeschlossene Befunde, ein beweisbar unveraenderter Zeichenraum in safe-next.ts, drei neue bzw. erweiterte Testdateien, Biome-Warnungen 445 -> 433.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
@apps/web/src/lib/safe-next.ts @apps/web/src/lib/safe-next.test.ts @apps/web/src/app/(auth)/login/page.tsx @apps/api/src/ldap/ldap.service.ts @apps/web/src/components/locale-switcher.tsx @biome.json
<measured_baseline_at_planning_time> Am 2026-09-21 bei der Planung selbst gemessen, Arbeitsverzeichnis sauber:
| Messung | Wert |
|---|---|
npx biome lint --reporter=json . Diagnosen gesamt |
446 (445 warning + 1 info) |
pnpm lint |
5/5 erfolgreich, 0 Befunde der Stufe error |
pnpm type-check |
4/4 erfolgreich |
pnpm --filter @tessera/web test |
67 Dateien / 477 Tests |
pnpm --filter @tessera/api test |
71 Dateien / 1136 Tests |
Die zwoelf Befunde im Auftrag, Regel fuer Regel nachgezaehlt (Testdateien getrennt):
| Regel | gesamt | echter Quelltext | Testdateien |
|---|---|---|---|
noControlCharactersInRegex |
3 | 3 | 0 |
useIterableCallbackReturn |
1 | 1 | 0 |
noGlobalIsNan |
4 | 4 | 0 |
noAssignInExpressions |
7 | 3 | 4 |
noDocumentCookie |
18 | 1 | 17 |
Rohbytes in apps/web/src/lib/safe-next.ts (selbst ausgelesen, nicht uebernommen):
Datei 2458 Byte, Zeile 18 lautet byteweise
63 6f 6e 73 74 20 46 4f 52 42 49 44 44 45 4e 5f 43 48 41 52 53 5f 52 45 20 3d 20 2f 5b 5c 5c 5c 73 00 2d 1f 7f 5d 2f 3b.
Die drei Nicht-Zeilenumbruch-Steuerbytes der Datei sitzen an Offset 826 (0x00),
828 (0x1F) und 829 (0x7F). Die Zeichenklasse ist also: Backslash, \s,
Bereich U+0000 bis U+001F, dazu U+007F. Die Transkription im Auftrag stimmt.
Drei Messungen, die den Plan tragen (jeweils mit einer Wegwerf-Datei geprueft und wieder entfernt):
- Biome beanstandet Steuerzeichen auch als Escape.
\u0000-\u001fund\x00-\x1ferzeugen genau dieselben zwei Warnungen wie die Rohbytes. Das blosse Umschreiben auf Escapes senkt die Warnungszahl also nicht — es braucht zusaetzlich einen Unterdrueckungskommentar. U+007F beanstandet Biome nicht; die Regel deckt nur U+0000..U+001F ab. Deshalb sind es 2 Befunde in dieser Datei und nicht 3. - Ein
// biome-ignoreunmittelbar ueber der Regex-Zeile unterdrueckt beide Spaltenbefunde dieser Zeile mit einem einzigen Kommentar. Steht der Kommentar dagegen eine Zeile zu hoch, bleibt der urspruengliche Befund stehen und Biome meldet zusaetzlichsuppressions/unused— die Warnungszahl steigt dann. Das ist die gefaehrlichste Falle dieses Vorgangs. - Die Form
for (let m = re.exec(t); m !== null; m = re.exec(t))wird vonnoAssignInExpressionsnicht beanstandet. Die drei exec-Schleifen lassen sich also ohne Unterdrueckungskommentar und ohne Verhaltensaenderung umschreiben.
Der Zeichenraum von sanitizeNextPath heute, vollstaendig ausgemessen
(U+0000..U+FFFF, Eingabe '/a' + Zeichen + 'b', abgewiesen = Rueckgabe '/'):
genau 54 Codepunkte werden abgewiesen —
0x00-0x1f, 0x20, 0x5c, 0x7f, 0xa0, 0x1680, 0x2000-0x200a,
0x2028, 0x2029, 0x202f, 0x205f, 0x3000, 0xfeff.
SHA-256 der 65536 Zeichen langen Bitmap aus '1'/'0':
3d58108b87e4641e506602cc701a66d11ec19cf20551937fada1826f50ecbe64.
Und vorab bewiesen, ebenfalls ueber alle 65536 Codepunkte: die Escape-Variante
/[\\\s\u0000-\u001f\u007f]/ trifft exakt dieselbe Menge wie die Variante
mit Rohbytes. Null Abweichungen.
</measured_baseline_at_planning_time>
| # | Datei | Regel | Urteil | Begruendung |
|---|---|---|---|---|
| 1 | apps/web/src/lib/safe-next.ts (2 Befunde) |
noControlCharactersInRegex |
Haertung | Kein Fehler heute, aber die Zeichenklasse des Open-Redirect-Schutzes haengt an drei rohen Bytes, die jedes Werkzeug in der Kette stillschweigend fressen kann — faellt das NUL weg, wird [\\\s-\u001f\u007f] daraus, ein Bereich, der ploetzlich andere Zeichen umfasst. |
| 2 | apps/api/src/ldap/ldap.service.ts:1662 |
noControlCharactersInRegex |
Absicht | .replace(/\x00/g, '\\00') ist die von RFC 4515 vorgeschriebene NUL-Maskierung in escapeLdapFilterValue — genau dieses Steuerzeichen zu treffen ist der Zweck; wer es entfernt, oeffnet LDAP-Filter-Injection. |
| 3 | apps/web/src/app/(portal)/modules/cert-manager/actions.ts:154 |
useIterableCallbackReturn |
gleichwertig | Es ist forEach, kein .every/.some/.map/.sort: der Pfeilausdruck gibt das undefined von FormData.append zurueck, forEach verwirft jeden Rueckgabewert. Rein kosmetisch. |
| 4-7 | TenderDetail.tsx (2x), InvoiceHistoryTable.tsx, ResultsList.tsx |
noGlobalIsNan |
gleichwertig | Alle vier Stellen lauten isNaN(d.getTime()). Date.prototype.getTime() liefert laut Spezifikation immer number, also findet gar keine Umwandlung statt. Der Tausch ist sicher, weil das Argument statisch number ist; er lohnt trotzdem, weil isNaN bei einer spaeteren Aenderung auf einen String stillschweigend umwandeln wuerde. |
| 8-10 | dkv-parser.service.ts:104, dkv-parser.validate.ts:139, icon-discovery.service.ts:182 |
noAssignInExpressions |
Absicht | Alle drei sind die idiomatische Form while ((m = re.exec(text)) !== null) mit globalem Regex — kein verrutschtes =. Keine der drei Variablen wird nach der Schleife gelesen. |
| 11 | apps/web/src/components/locale-switcher.tsx:15 |
noDocumentCookie |
Haertung | path=/ und max-age=31536000 sind gesetzt und richtig, die Sprache bleibt also erhalten — kein Persistenzdefekt. Es fehlt SameSite: das Cookie haengt damit am Browservorgabewert statt an einer Festlegung. Die Cookie-Store-API, die Biome vorschlaegt, fehlt in WebKit und damit im Tauri-Wrapper auf Linux/macOS. |
- Test A "die abgewiesene Zeichenmenge ist und bleibt genau diese 54 Codepunkte":
Schleife ueber cp = 0 bis 0xffff, Eingabe `'/a' + String.fromCharCode(cp) + 'b'`,
abgewiesen genau dann, wenn `sanitizeNextPath(...)` den Wert `'/'` liefert.
Die gesammelte Liste der abgewiesenen Codepunkte muss exakt gleich sein zu
der im Plan gemessenen Liste (0x00-0x1f, 0x20, 0x5c, 0x7f, 0xa0, 0x1680,
0x2000-0x200a, 0x2028, 0x2029, 0x202f, 0x205f, 0x3000, 0xfeff — 54 Stueck).
Die Erwartung wird als Liste hart hinterlegt, nicht aus einem Praedikat
erzeugt: eine aus derselben Regel abgeleitete Erwartung wuerde jede
Aenderung der Regel mitmachen und damit nichts beweisen.
- Test B "die Datei enthaelt kein rohes Steuerzeichen": liest die eigene
Quelldatei ueber `node:fs` als Buffer und zaehlt Bytes < 0x20 ausser 0x0a,
plus 0x7f. Erwartung: 0.
- Test C "der Zwei-Zeichen-Praefixtest bleibt scharf": `'//host'`,
`'/\\host'` und `'/'` + ein U+0000 landen weiterhin auf `'/'`.
Test A muss **vor** dem Umbau geschrieben werden und **gegen die heutige
Fassung gruen** sein — das ist der Sinn: er haelt den Ist-Zustand fest.
Test B ist vor dem Umbau rot (3 Bytes) und nach dem Umbau gruen.
Schritt 1 — Ausgangswert sichern, bevor irgendetwas geaendert wird. Test A und
Test C in safe-next.test.ts ergaenzen, Test B ebenfalls ergaenzen aber zunaechst
sein Rotsein zur Kenntnis nehmen. `npx vitest run src/lib/safe-next.test.ts` in
apps/web laufen lassen: A und C gruen, B rot. Zusaetzlich den SHA-256 der
Bitmap aus Test A ausgeben lassen oder einmalig berechnen und notieren; er muss
`3d58108b87e4641e506602cc701a66d11ec19cf20551937fada1826f50ecbe64` lauten.
Weicht er ab, sofort anhalten und melden — dann ist der Arbeitsbaum nicht der,
auf dem dieser Plan gemessen wurde.
Schritt 2 — den Umbau. In safe-next.ts die Zeile mit `FORBIDDEN_CHARS_RE` so
ersetzen, dass die Zeichenklasse aus Backslash, `\s`, dem Bereich `\u0000` bis
`\u001f` und `\u007f` besteht, geschrieben ausschliesslich mit
Unicode-Escapes. Danach enthaelt die Datei keine Rohbytes mehr (D-01). Die
Reihenfolge der Klassenglieder unveraendert lassen.
Schritt 3 — den Kommentar berichtigen. Der bestehende Zweizeiler ueber der
Konstante behauptet heute, die Zeichen seien "als Unicode-Escapes, nicht \x,
weil safe-next.ts auch am Edge laeuft" geschrieben. Das war doppelt falsch:
sie waren keine Escapes, und zwischen `\x` und `\u` gibt es zur Laufzeit
keinerlei Unterschied, am Edge so wenig wie im Browser. Neu formulieren: was
die Klasse trifft (Backslash, jegliches Whitespace, U+0000 bis U+001F, U+007F),
und die tatsaechliche Randbedingung der Datei — sie wird von der
Edge-Middleware und vom Client importiert und darf deshalb keine Node- oder
DOM-APIs verwenden. Nicht behaupten, Escapes seien am Edge noetig.
Schritt 4 — die beiden Biome-Befunde unterdruecken. Unmittelbar, ohne
Leerzeile, ueber die Regex-Zeile einen `biome-ignore`-Kommentar fuer
`lint/suspicious/noControlCharactersInRegex` setzen. Begruendung im
Repo-Stil (siehe die vier vorhandenen Beispiele, deutsch, nennt die Folge):
dass die Regel den Bereich auch in Escape-Schreibweise beanstandet, dass
genau dieser Bereich der Zweck des Ausdrucks ist, und dass er den
Weiterleitungsschutz T-gyd-01 traegt. Die Platzierung ist kritisch: eine
Zeile zu hoch und Biome meldet den alten Befund weiter und zusaetzlich einen
unbenutzten Unterdrueckungskommentar.
Schritt 5 — beweisen. Test A erneut laufen lassen; er muss ohne jede Aenderung
an der Erwartungsliste gruen bleiben. Test B ist jetzt gruen.
WERKZEUGFALLE, bei der Planung selbst hineingetappt: Die Schreibwerkzeuge
(Write, Edit) behandeln ihren Inhalt wie eine JSON-Zeichenkette und wandeln
eine Sequenz aus Backslash, kleinem `u` und vier Hexziffern **still in das
Zeichen selbst um**. Wer die neue Regex-Zeile auf diesem Weg schreibt,
erzeugt exakt wieder die rohen Steuerbytes, die dieser Task beseitigen soll —
Test B faellt dann durch, und beim fluechtigen Hinsehen sieht die Datei
richtig aus. Abhilfe: die Zeile mit einem Platzhalter statt des Backslashs
schreiben und den Platzhalter anschliessend per `python3` durch `chr(92)`
ersetzen, oder die Zeile gleich vollstaendig per `python3` in die Datei
schreiben. Danach zwingend der Byte-Scan aus `<verify>`, bevor irgendetwas
als fertig gilt.
Was NICHT passiert (D-04): der Zwei-Zeichen-Praefixtest bleibt wie er ist, es
kommt keine Liste bekannter Schemata hinzu, `MAX_NEXT_PATH_LENGTH` bleibt bei
2048, die Reihenfolge der Pruefungen bleibt, die bestehenden Tests 1-9 werden
nicht angefasst. Keine Abhaengigkeit, keine Formatierung ausserhalb dieser
Datei (D-05).
cd apps/web && npx vitest run src/lib/safe-next.test.ts 2>&1 | tail -6
python3 -c "b=open('apps/web/src/lib/safe-next.ts','rb').read(); c=[i for i,x in enumerate(b) if (x<0x20 and x!=0x0a) or x==0x7f]; print('CTRL_BYTES', len(c), c); assert len(c)==0, 'rohe Steuerbytes noch vorhanden'"
npx biome lint apps/web/src/lib/safe-next.ts 2>&1 | tail -3
Vitest meldet `Test Files 1 passed`, Tests >= 12 passed, kein failed.
Der Byte-Scan gibt `CTRL_BYTES 0 []` aus und bricht nicht ab.
Biome meldet fuer die Datei `Found 0 warnings` (weder ein
Steuerzeichen-Befund noch ein unbenutzter Unterdrueckungskommentar).
Test A erzwingt weiterhin exakt 54 abgewiesene Codepunkte — dieselbe Liste
wie vor dem Umbau, damit ist der Zeichenraum nachweislich identisch (D-01).
Die Tests 5 bis 9 aus der Bestandsdatei laufen unveraendert durch (D-04).
Eine Zeile Regex plus Tests; jederzeit per git revert zurueckzunehmen, und die Testdatei haelt den Ist-Zustand fest.
Task 2: isNaN und forEach — vier plus ein gleichwertiger Tausch
apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx, apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx, apps/web/src/app/(portal)/modules/dkv-fleet/components/InvoiceHistoryTable.tsx, apps/web/src/app/(portal)/modules/cert-manager/actions.ts
TenderDetail.tsx Zeile 40-70, ResultsList.tsx Zeile 45-55,
InvoiceHistoryTable.tsx Zeile 25-40, cert-manager/actions.ts Zeile 148-160.
Umsetzt D-02 und D-03.
Fuenf Stellen, alle als gleichwertig beurteilt — die Aenderung ist Haertung,
kein Defektbehebung, und so ist sie auch in der Zusammenfassung zu benennen
(D-02: das offene Eingestaendnis ist das erwuenschte Ergebnis, das stille
Umdeuten zum Fehler nicht).
Vier Stellen `isNaN(d.getTime())` auf `Number.isNaN(d.getTime())` umstellen:
TenderDetail.tsx Zeile 44 und Zeile 65, InvoiceHistoryTable.tsx Zeile 33,
ResultsList.tsx Zeile 51. Der Tausch ist deshalb sicher, weil `getTime()`
immer `number` liefert — die Umwandlung, vor der die Regel warnt, findet an
diesen Stellen gar nicht statt. Der Nutzen liegt in der Zukunft: sobald dort
einmal ein String aus einer API oder einem Formular ankaeme, wuerde das
globale `isNaN` ihn stillschweigend umwandeln und eine kaputte Eingabe als
gueltiges Datum durchwinken. Die bestehenden Rueckfallwerte
(`noDeadlineLabel`, `'–'`, `isoString`) unveraendert lassen, ebenso den
Kommentar ueber Zeile 33 in InvoiceHistoryTable.tsx (WR-03), der weiterhin
stimmt. Die vorhandenen `biome-ignore`-Kommentare fuer
`useExhaustiveDependencies` in InvoiceHistoryTable.tsx Zeile 84 und
ResultsList.tsx Zeile 141 nicht beruehren.
In cert-manager/actions.ts Zeile 154 die Pfeilfunktion mit Kurzform-Rumpf auf
einen Block-Rumpf umstellen, sodass der Rueckgabewert von `FormData.append`
nicht mehr aus der Schleifenfunktion herausgereicht wird. `forEach` verwirft
ihn ohnehin — deshalb ist das eine Lesbarkeitsaenderung ohne jede Wirkung.
Eine Umstellung auf `for...of` ist ebenso zulaessig, aendert aber mehr Zeilen
als noetig. Reihenfolge der `form.append`-Aufrufe strikt beibehalten: erst
alle Dateien, dann `outputFormat`, dann optional `password` — der Server
liest das Multipart-Formular in dieser Reihenfolge.
npx biome lint --reporter=json --max-diagnostics=5000 . 2>/dev/null | python3 -c "import sys,json,collections; d=json.load(sys.stdin); c=collections.Counter(x['category'] for x in d['diagnostics']); print('noGlobalIsNan', c['lint/suspicious/noGlobalIsNan']); print('useIterableCallbackReturn', c['lint/suspicious/useIterableCallbackReturn']); assert c['lint/suspicious/noGlobalIsNan']==0 and c['lint/suspicious/useIterableCallbackReturn']==0"
pnpm --filter @tessera/web test 2>&1 | tail -5
pnpm type-check 2>&1 | tail -4
Die Zaehlung aus der Biome-JSON-Ausgabe meldet `noGlobalIsNan 0` und
`useIterableCallbackReturn 0` und bricht nicht ab. Die Zahlen stammen aus
dem Feld `category` der Diagnosen, nicht aus einer Textsuche im Quelltext —
ein Kommentar im Code kann das Tor deshalb nicht verfaelschen.
`pnpm --filter @tessera/web test` meldet mindestens 67 Dateien und
mindestens 477 Tests, alle passed.
`pnpm type-check` meldet 4/4 erfolgreich.
Kein Rueckfallwert und keine append-Reihenfolge wurde veraendert (D-03).
Task 3: exec-Schleifen, LDAP-NUL-Maskierung und das Sprachumschalter-Cookie
apps/api/src/dkv/dkv-parser.service.ts, apps/api/src/dkv/dkv-parser.service.spec.ts, apps/api/src/dkv/dkv-parser.validate.ts, apps/api/src/favorites/icon-discovery.service.ts, apps/api/src/ldap/ldap.service.ts, apps/web/src/components/locale-switcher.tsx, apps/web/src/components/locale-switcher.test.tsx
apps/api/src/dkv/dkv-parser.service.ts Zeile 34-115 (parsePdf, extractText, parseDkvText),
apps/api/src/dkv/dkv-parser.validate.ts Zeile 128-150,
apps/api/src/favorites/icon-discovery.service.ts Zeile 177-190,
apps/api/src/ldap/ldap.service.ts Zeile 1650-1675 (escapeLdapFilterValue und die NOTE darunter),
apps/web/src/components/locale-switcher.tsx,
apps/web/src/i18n/request.ts,
apps/api/src/tenders/tender-mail.service.spec.ts (Vorbild fuer vi.mock auf ein Paket),
apps/api/src/favorites/icon-discovery.service.spec.ts (deckt parseAttributes ueber discoverFavoriteIconUrl ab).
Umsetzt D-02, D-03 und D-05.
Zwei neue Tests, beide **vor** der jeweiligen Aenderung geschrieben und gegen
die heutige Fassung gruen:
- `apps/api/src/dkv/dkv-parser.service.spec.ts` (neu): `pdf-parse` per
`vi.mock` durch eine Attrappe ersetzen, deren `getText()` einen fest
hinterlegten DKV-Beispieltext mit **zwei** Fahrzeugbloecken liefert
(`VEHICLE: ... CARD NO.: ...`, je eine tabulatorgetrennte Transaktionszeile,
dazu eine Rechnungsnummer im Format DD/DDDDDDDDD/DDD gefolgt von einem
Datum in der naechsten Zeile). `destroy()` als no-op. Ein Test:
`parsePdf` liefert zwei Fahrzeugbloecke mit den erwarteten Kennzeichen und
Kartennummern, plus die erwartete Rechnungsnummer und das erwartete
Rechnungsdatum. Dieser Test ist das Rueckfall-Tor fuer den
Schleifenumbau — `parseDkvText` hat heute keine eigene Abdeckung.
- `apps/web/src/components/locale-switcher.test.tsx` (neu): den Setter von
`document.cookie` per `Object.defineProperty` durch eine Attrappe ersetzen,
die Komponente rendern, den Knopf klicken und die genau **eine**
geschriebene Zeichenkette pruefen: sie enthaelt `NEXT_LOCALE=en`,
`path=/`, `max-age=31536000` und — nach der Aenderung — `SameSite=Lax`.
Vor der Aenderung faellt nur die SameSite-Zusicherung durch; das ist
beabsichtigt und zeigt genau die Luecke.
Teil A — die drei exec-Schleifen (Urteil: Absicht, kein verrutschtes `=`).
Alle drei sind die korrekte Standardform. Sie werden trotzdem umgeschrieben,
weil es eine verhaltensgleiche Schreibweise gibt, die ohne
Unterdrueckungskommentar auskommt: die Zuweisung wandert in den Kopf einer
`for`-Schleife, deren Initialisierung den ersten `exec`-Aufruf macht, deren
Bedingung auf `null` prueft und deren Fortschaltung den naechsten
`exec`-Aufruf macht. Bei Planung nachgemessen: diese Form loest die Regel
nicht aus. Die Abfolge der `exec`-Aufrufe und der Rumpfdurchlaeufe ist
identisch, der globale Regex behaelt seinen `lastIndex`-Fortschritt.
Betroffen: `dkv-parser.service.ts` Zeile 103/104 (`match`),
`dkv-parser.validate.ts` Zeile 138/139 (`match`),
`icon-discovery.service.ts` Zeile 180/182 (`m`). In allen drei Faellen wird
die Variable nach der Schleife nicht mehr gelesen — nachgeprueft —, die
vorgezogene `let`-Deklaration entfaellt also ersatzlos. Die Regex-Literale
selbst, die Rumpfinhalte und die `.trim()`-Aufrufe bleiben Zeichen fuer
Zeichen unveraendert. `dkv-parser.validate.ts` ist ein eigenstaendiges
Pruefskript, das nur per `node --experimental-strip-types` laeuft; es wird
mitgeaendert, damit es nicht von der Service-Fassung abdriftet, und von
`pnpm type-check` mit abgedeckt.
Teil B — die NUL-Maskierung im LDAP-Filter (Urteil: Absicht). An
`LdapService.escapeLdapFilterValue` wird **nichts** geaendert. Der Ausdruck
trifft U+0000 absichtlich, weil RFC 4515 genau dieses Zeichen als `\00`
verlangt; die Funktion ist die Injection-Sperre fuer die drei Filterbauten in
Zeile 576, 1142 und 1228. Stattdessen unmittelbar ueber die betroffene Zeile
ein `biome-ignore` fuer `lint/suspicious/noControlCharactersInRegex` setzen,
mit deutscher Begruendung, die den RFC und die Folge des Entfernens nennt
(ein unmaskiertes NUL kann den Filter beim Verzeichnisserver abschneiden).
Die Escape-Reihenfolge — Backslash zuerst, dann Stern, Klammer auf, Klammer
zu, NUL — nicht antasten: der Backslash muss zuerst kommen, sonst werden die
eigenen Ersetzungen erneut maskiert.
Teil C — das Sprachumschalter-Cookie (Urteil: Haertung). `path=/` stimmt und
`max-age=31536000` stimmt; `i18n/request.ts` liest `NEXT_LOCALE`
serverseitig und bekommt das Cookie heute auch — es liegt also kein
Persistenzdefekt vor, und das ist so zu benennen. Ergaenzt wird `SameSite=Lax`,
damit die Uebertragung bei der Seitennavigation festgelegt ist statt vom
Vorgabewert des jeweiligen Browsers abzuhaengen; `Lax` und nicht `Strict`,
weil das Cookie bei einer Navigation von aussen auf die Seite mitkommen muss,
sonst faellt die Oberflaeche auf Deutsch zurueck. Kein `Secure` setzen: die
Entwicklungsumgebung laeuft auf `http://localhost:3000`, und das Cookie traegt
keine Berechtigung, nur die Sprachwahl. Der Schreibzugriff bleibt
`document.cookie`; darueber ein `biome-ignore` fuer
`lint/suspicious/noDocumentCookie` mit der Begruendung, dass die von Biome
vorgeschlagene Cookie-Store-API in WebKit fehlt und die Sprachumschaltung
damit im Tauri-Wrapper auf Linux und macOS wirkungslos waere.
Fuer alle drei Unterdrueckungskommentare in diesem Vorgang gilt: sie stehen
direkt ueber der beanstandeten Zeile, ohne Leerzeile dazwischen. Sitzt einer
falsch, bleibt der alte Befund stehen und Biome meldet zusaetzlich einen
unbenutzten Kommentar — die Warnungszahl steigt dann statt zu fallen und das
Tor in `<done>` schlaegt fehl.
Keine neue Abhaengigkeit, keine Versionsanhebung, keine Formatierung ueber
die genannten Dateien hinaus (D-05).
pnpm --filter @tessera/api test 2>&1 | tail -5
pnpm --filter @tessera/web test 2>&1 | tail -5
pnpm type-check 2>&1 | tail -4
npx biome lint --reporter=json --max-diagnostics=5000 . 2>/dev/null | python3 -c "import sys,json,collections; d=json.load(sys.stdin); c=collections.Counter(x['category'] for x in d['diagnostics']); s=collections.Counter(x['severity'] for x in d['diagnostics']); print('total',len(d['diagnostics']),'sev',dict(s)); print({k:c[k] for k in ['lint/suspicious/noControlCharactersInRegex','lint/suspicious/useIterableCallbackReturn','lint/suspicious/noGlobalIsNan','lint/suspicious/noAssignInExpressions','lint/suspicious/noDocumentCookie','suppressions/unused']}); assert c['lint/suspicious/noControlCharactersInRegex']==0; assert c['lint/suspicious/noAssignInExpressions']==4; assert c['lint/suspicious/noDocumentCookie']==17; assert c['suppressions/unused']==0; assert s['error']==0; assert len(d['diagnostics'])==434"
pnpm lint 2>&1 | tail -4
`pnpm --filter @tessera/api test` meldet 72 Dateien und mindestens 1137 Tests,
alle passed (die 71/1136 von vorher plus die neue Parser-Spezifikation).
`pnpm --filter @tessera/web test` meldet mindestens 68 Dateien und mindestens
480 Tests, alle passed.
`pnpm type-check` meldet 4/4 erfolgreich.
Die Biome-Zaehlung meldet `total 434`, `sev {'warning': 433, 'info': 1}` und
bricht bei keiner der sechs Zusicherungen ab: die drei Steuerzeichen-Befunde
sind weg, `noAssignInExpressions` steht bei 4 (die vier verbliebenen liegen
ausschliesslich in Testdateien und waren nie im Auftrag),
`noDocumentCookie` steht bei 17 (ebenfalls samtlich Testdateien), es gibt
keinen unbenutzten Unterdrueckungskommentar und keinen Befund der Stufe error.
434 = 446 minus die zwoelf bearbeiteten Befunde — die Zahl faellt also genau
um das, was bearbeitet wurde, und waechst nirgends sonst (D-06).
`pnpm lint` meldet `5 successful, 5 total`.
`escapeLdapFilterValue` ist zeichengleich zur Ausgangsfassung geblieben (D-03).
Mechanische Umschreibungen plus zwei Kommentare; die beiden neuen Spezifikationen halten das Verhalten fest, ein Zurueckrollen ist folgenlos.
<threat_model>
ASVS-Stufe 1, blockierend ab Schweregrad high. Keine Paketinstallation in diesem Vorgang (D-05), deshalb kein Paket-Legitimitaetstor.
Trust Boundaries
| Grenze | Beschreibung |
|---|---|
Adresszeile des Browsers -> next-Parameter -> sanitizeNextPath -> window.location.href |
Vollstaendig angreiferkontrollierter Wert. In login/page.tsx:37 wird der Rueckgabewert direkt der Adresszeile zugewiesen; zwischen der Funktion und der Navigation liegt nichts mehr. |
Browser -> Edge-Middleware -> buildNextParam |
Die Middleware importiert nur buildNextParam und nicht sanitizeNextPath. Der Schutz laeuft also ausschliesslich im Client. Die Doppelnutzung der Datei bedeutet praktisch nur eine Auflage: keine Node- und keine DOM-APIs in safe-next.ts. Zwischen \x- und \u-Escapes gibt es zur Laufzeit keinen Unterschied, am Edge so wenig wie im Browser — die gegenteilige Behauptung im heutigen Kommentar ist gegenstandslos und wird in Task 1 berichtigt. |
Nutzereingabe / Gruppen-DN aus der Datenbank -> escapeLdapFilterValue -> LDAP-Filterzeichenkette -> Verzeichnisserver |
Die Maskierung ist die einzige Sperre gegen Filter-Injection; das Verzeichnis wird nur lesend gebunden. |
Client -> document.cookie NEXT_LOCALE -> cookies().get('NEXT_LOCALE') in i18n/request.ts |
Vom Nutzer setzbarer Wert ohne Berechtigung; steuert nur die Sprachauswahl. |
DKV-PDF aus dem Postfach -> parsePdf -> Fahrzeug- und Transaktionsdaten |
Fremdformat; ein stiller Parserfehler erzeugt falsche Rechnungsdaten statt eines Abbruchs. |
STRIDE Threat Register
| Threat ID | Kategorie | Komponente | Schweregrad | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-i8x-01 | Spoofing | FORBIDDEN_CHARS_RE in safe-next.ts |
high | mitigate | Faellt beim Umschreiben ein Klassenglied weg oder verrutscht der Bereichsstrich, veraendert sich die abgewiesene Zeichenmenge unbemerkt — und genau diese Klasse haelt Whitespace-Schmuggel und Steuerzeichen aus dem Ziel heraus, das login/page.tsx:37 in die Adresszeile schreibt. Task 1 haelt die Menge mit einem Test ueber alle 65536 Codepunkte gegen eine hart hinterlegte Liste von 54 Codepunkten fest, geschrieben vor dem Umbau und gruen gegen die Altfassung. Zusaetzlich vorab bewiesen: die Escape-Variante trifft dieselbe Menge, null Abweichungen. |
| T-i8x-02 | Tampering | Zwei-Zeichen-Praefixtest in sanitizeNextPath |
high | mitigate | Ein "Vereinfachen" von raw[1] === '/' || raw[1] === '\\' wuerde protokoll-relative Ziele wie //boeser.host wieder durchlassen, die der Browser als absolute Adresse liest. D-04 verbietet die Aenderung; Task 1 aendert ausschliesslich die Regex-Zeile und die zwei Kommentarzeilen, Test C und die Bestandstests 7 und 8 sichern den Praefixtest ab. |
| T-i8x-03 | Tampering | LdapService.escapeLdapFilterValue |
high | mitigate | Wer das als "Steuerzeichen-Befund" behandelt und den NUL-Ersatz entfernt oder die Reihenfolge der Ersetzungen dreht, oeffnet LDAP-Filter-Injection bzw. laesst den Backslash die eigenen Ersetzungen doppelt maskieren. Task 3 Teil B aendert die Funktion nicht, sondern setzt nur einen Unterdrueckungskommentar; <done> fordert Zeichengleichheit zur Ausgangsfassung. |
| T-i8x-04 | Tampering | parseDkvText und parseAttributes, Umbau der exec-Schleifen |
medium | mitigate | Ein falsch gesetzter Fortschaltungsausdruck ergibt eine Endlosschleife oder ueberspringt jeden zweiten Treffer — bei der Rechnungsauswertung faellt das erst an falschen Zahlen auf. parseDkvText hat heute keine Abdeckung; Task 3 legt dafuer eine Spezifikation mit zwei Fahrzeugbloecken an, bevor umgebaut wird. parseAttributes ist ueber icon-discovery.service.spec.ts ("extracts the apple-touch-icon from page HTML") bereits abgedeckt. |
| T-i8x-05 | Information Disclosure | NEXT_LOCALE-Cookie | low | mitigate | Ohne ausdrueckliches SameSite haengt die Uebertragung am Vorgabewert des Browsers; ein Browser ohne Lax-Vorgabe schickt das Cookie auch seitenuebergreifend mit. Das Cookie traegt keine Berechtigung, der Schaden ist entsprechend gering. Task 3 Teil C setzt SameSite=Lax und sichert die vollstaendige geschriebene Zeichenkette mit einem Test ab. |
| T-i8x-06 | Repudiation | Unterdrueckungskommentare in allen drei Dateien | low | mitigate | Ein falsch platzierter biome-ignore laesst den urspruenglichen Befund stehen und erzeugt zusaetzlich suppressions/unused — die sicherheitsrelevante Regel waere dann still weiter offen, waehrend die Zusammenfassung sie als erledigt fuehrt. Das Tor in Task 3 zaehlt suppressions/unused ausdruecklich auf 0 und die Gesamtzahl auf exakt 434. |
| T-i8x-07 | Tampering | Messtor selbst | low | accept | Die Zaehlungen lesen das Feld category aus Biomes JSON-Ausgabe, nie den Quelltext per Textsuche. Ein Regelname im Unterdrueckungskommentar kann das Tor deshalb nicht verfaelschen. Kein weiterer Aufwand noetig. |
</threat_model>
Nach allen drei Tasks, im sauberen Arbeitsbaum, vom Projektwurzelverzeichnis aus:
pnpm lint— erwartet5 successful, 5 total.- Die Biome-JSON-Zaehlung aus Task 3 — erwartet
total 434,sev {'warning': 433, 'info': 1},noControlCharactersInRegex 0,useIterableCallbackReturn 0,noGlobalIsNan 0,noAssignInExpressions 4,noDocumentCookie 17,suppressions/unused 0. pnpm type-check— erwartet4 successful, 4 total.pnpm --filter @tessera/web test— erwartet >= 68 Dateien, >= 480 Tests, 0 failed.pnpm --filter @tessera/api test— erwartet 72 Dateien, >= 1137 Tests, 0 failed.- Byte-Scan von
apps/web/src/lib/safe-next.ts— erwartetCTRL_BYTES 0 []. git status --porcelain -- apps packages— erwartet genau die 13 Dateien ausfiles_modified, keine weitere. Schlaegt das fehl, wurde formatiert oder umgeraeumt, was D-05 verbietet. (.planning/ist hier bewusst ausgeklammert: Plan und Zusammenfassung gehoeren dazu.)git diff -- package.json '*/package.json' pnpm-lock.yaml— erwartet leer: keine neue Abhaengigkeit, keine Versionsanhebung (D-05).
Kein laufender Stack noetig: alle zwoelf Befunde liessen sich statisch beurteilen, und die zwei riskanten Stellen (Zeichenraum des Weiterleitungsschutzes, Parser-Schleifen) sind mit Tests besser abgedeckt als mit einer Bedienprobe. Die Aenderung am Sprachumschalter ist im Browser nachpruefbar, falls der Nutzer es wuenscht — sie ist es nicht wert, dafuer einen Stack hochzufahren.
<success_criteria>
- Zwoelf Befunde abgeschlossen, jeder mit Urteil und einer Begruendung, die benennt, was ohne die Entscheidung schiefginge (D-02). Die sieben als gleichwertig bzw. absichtlich beurteilten Stellen sind als solche benannt und nicht nachtraeglich zum Fehler erklaert.
sanitizeNextPathweist nach dem Umbau exakt dieselben 54 Codepunkte ab wie vorher, nachgewiesen ueber alle 65536 Codepunkte aus U+0000..U+FFFF (D-01).apps/web/src/lib/safe-next.tsenthaelt kein rohes Steuerbyte mehr, und der Kommentar beschreibt jetzt zutreffend, was in der Zeile darunter steht (D-01).- Der Weiterleitungsschutz wurde nicht gelockert: Praefixtest, Laengenbegrenzung, Pruefreihenfolge und die bewusst fehlende Schema-Liste unveraendert (D-04).
- Ausser den drei Stellen, an denen eine Haertung ausdruecklich beschlossen wurde
(Escape-Schreibweise,
Number.isNaN,SameSite=Lax), aendert sich kein beobachtbares Verhalten (D-03). - Keine neue Abhaengigkeit, keine Versionsanhebung, keine Formatierung ausserhalb der 13 genannten Dateien (D-05).
pnpm lintbleibt 5/5 bei 0 Befunden der Stufe error; die Warnungszahl faellt von 445 auf 433 und waechst an keiner anderen Stelle (D-06).noArrayIndexKeyundnoNonNullAssertionwurden nicht angefasst — sie gehoeren in den Folgevorgang.noUselessSwitchCasebleibt der dokumentierte Nicht-Fix aus 260921-bi2.
</success_criteria>
Create `.planning/quick/260921-i8x-fehlerverdaechtige-lint-klassen-steuerze/260921-i8x-SUMMARY.md` when done.Die Zusammenfassung enthaelt die Urteilstabelle aus <verdicts> mit dem
tatsaechlich Umgesetzten je Zeile, die Vorher-/Nachher-Zahlen aus Abschnitt
<verification> und — ausdruecklich — den Satz, dass sieben der zwoelf Befunde
keine Fehler waren.