fix(web): Steuerzeichen in safe-next.ts als Unicode-Escapes statt Rohbytes
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
This commit is contained in:
+530
@@ -0,0 +1,530 @@
|
||||
---
|
||||
phase: quick-260921-i8x
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
autonomous: true
|
||||
requirements: [D-01, D-02, D-03, D-04, D-05, D-06]
|
||||
files_modified:
|
||||
- apps/web/src/lib/safe-next.ts
|
||||
- apps/web/src/lib/safe-next.test.ts
|
||||
- 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
|
||||
- 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
|
||||
|
||||
estimate:
|
||||
tokens: 90000
|
||||
raw_tokens: 45000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "sanitizeNextPath weist nach dem Umbau exakt dieselben 54 Codepunkte aus U+0000..U+FFFF ab wie vorher — nachgewiesen, nicht behauptet (D-01)."
|
||||
- "apps/web/src/lib/safe-next.ts enthaelt kein einziges rohes Steuerbyte mehr (vorher 3: Offset 826/828/829)."
|
||||
- "Jeder der 12 Befunde hat eine Entscheidung und eine Begruendung, die benennt, was sonst schiefginge (D-02)."
|
||||
- "Kein Verhalten aendert sich ausser an einer Stelle, an der ein echter Defekt benannt wurde (D-03)."
|
||||
- "Der Open-Redirect-Schutz bleibt unveraendert streng: kein Schema-Allowlist, kein gelockerter Zwei-Zeichen-Praefixtest (D-04)."
|
||||
- "pnpm lint bleibt 5/5 ohne Befund der Stufe error; Biome meldet 433 Warnungen statt 445 und keinen einzigen suppressions/unused-Befund (D-06)."
|
||||
- "Die Testsuiten bleiben gruen und wachsen: apps/web >= 68 Dateien / >= 480 Tests, apps/api 72 Dateien / >= 1137 Tests."
|
||||
artifacts:
|
||||
- apps/web/src/lib/safe-next.ts
|
||||
- apps/web/src/lib/safe-next.test.ts
|
||||
- apps/api/src/dkv/dkv-parser.service.spec.ts
|
||||
- apps/web/src/components/locale-switcher.test.tsx
|
||||
key_links:
|
||||
- "login/page.tsx Zeile 37: window.location.href = sanitizeNextPath(next) — der Rueckgabewert landet ungefiltert in der Adresszeile."
|
||||
- "middleware.ts importiert nur buildNextParam; safe-next.ts muss deshalb weiterhin frei von Node- und DOM-APIs bleiben."
|
||||
- "LdapService.escapeLdapFilterValue speist drei Filterbauten (Zeile 576, 1142, 1228) — die Escape-Kette ist die LDAP-Injection-Sperre."
|
||||
- "i18n/request.ts liest NEXT_LOCALE serverseitig; was locale-switcher.tsx schreibt, muss dort ankommen."
|
||||
---
|
||||
|
||||
<objective>
|
||||
Zwoelf fehlerverdaechtige Lint-Befunde aus fuenf Regelklassen einzeln beurteilen und
|
||||
abschliessen — jeder mit Urteil und Begruendung (D-02), ohne Verhaltensaenderung
|
||||
ausser an nachgewiesenen Defekten (D-03).
|
||||
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||
@~/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/STATE.md
|
||||
@CLAUDE.md
|
||||
|
||||
@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
|
||||
</context>
|
||||
|
||||
<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):**
|
||||
|
||||
1. Biome beanstandet Steuerzeichen **auch als Escape**. `\u0000-\u001f` und
|
||||
`\x00-\x1f` erzeugen 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.
|
||||
2. Ein `// biome-ignore` unmittelbar 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 zusaetzlich `suppressions/unused` — die Warnungszahl steigt dann.
|
||||
Das ist die gefaehrlichste Falle dieses Vorgangs.
|
||||
3. Die Form `for (let m = re.exec(t); m !== null; m = re.exec(t))` wird von
|
||||
`noAssignInExpressions` **nicht** 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>
|
||||
|
||||
<verdicts>
|
||||
Das Urteil je Befund, aus dem gelesenen Quelltext, nicht aus der Regel (D-02):
|
||||
|
||||
| # | 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. |
|
||||
</verdicts>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: safe-next.ts — Steuerzeichen als Escapes, Zeichenmenge beweisbar unveraendert</name>
|
||||
<files>apps/web/src/lib/safe-next.ts, apps/web/src/lib/safe-next.test.ts</files>
|
||||
<read_first>
|
||||
apps/web/src/lib/safe-next.ts (vollstaendig, inklusive Rohbytes),
|
||||
apps/web/src/lib/safe-next.test.ts (Tests 1-9),
|
||||
apps/web/src/app/(auth)/login/page.tsx Zeile 30-40 (die Senke: window.location.href),
|
||||
apps/web/src/middleware.ts Zeile 1-5 und 94-100 (importiert nur buildNextParam).
|
||||
Umsetzt D-01 und D-04.
|
||||
</read_first>
|
||||
<behavior>
|
||||
Zuerst die Tests, dann der Umbau. Neu in safe-next.test.ts:
|
||||
|
||||
- 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.
|
||||
</behavior>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/web && npx vitest run src/lib/safe-next.test.ts 2>&1 | tail -6</automated>
|
||||
<automated>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'"</automated>
|
||||
<automated>npx biome lint apps/web/src/lib/safe-next.ts 2>&1 | tail -3</automated>
|
||||
</verify>
|
||||
<done>
|
||||
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).
|
||||
</done>
|
||||
<reversibility rating="reversible">Eine Zeile Regex plus Tests; jederzeit per git revert zurueckzunehmen, und die Testdatei haelt den Ist-Zustand fest.</reversibility>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: isNaN und forEach — vier plus ein gleichwertiger Tausch</name>
|
||||
<files>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</files>
|
||||
<read_first>
|
||||
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.
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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"</automated>
|
||||
<automated>pnpm --filter @tessera/web test 2>&1 | tail -5</automated>
|
||||
<automated>pnpm type-check 2>&1 | tail -4</automated>
|
||||
</verify>
|
||||
<done>
|
||||
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).
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: exec-Schleifen, LDAP-NUL-Maskierung und das Sprachumschalter-Cookie</name>
|
||||
<files>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</files>
|
||||
<read_first>
|
||||
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.
|
||||
</read_first>
|
||||
<behavior>
|
||||
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.
|
||||
</behavior>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/api test 2>&1 | tail -5</automated>
|
||||
<automated>pnpm --filter @tessera/web test 2>&1 | tail -5</automated>
|
||||
<automated>pnpm type-check 2>&1 | tail -4</automated>
|
||||
<automated>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"</automated>
|
||||
<automated>pnpm lint 2>&1 | tail -4</automated>
|
||||
</verify>
|
||||
<done>
|
||||
`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).
|
||||
</done>
|
||||
<reversibility rating="reversible">Mechanische Umschreibungen plus zwei Kommentare; die beiden neuen Spezifikationen halten das Verhalten fest, ein Zurueckrollen ist folgenlos.</reversibility>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<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>
|
||||
|
||||
<verification>
|
||||
|
||||
Nach allen drei Tasks, im sauberen Arbeitsbaum, vom Projektwurzelverzeichnis aus:
|
||||
|
||||
1. `pnpm lint` — erwartet `5 successful, 5 total`.
|
||||
2. 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`.
|
||||
3. `pnpm type-check` — erwartet `4 successful, 4 total`.
|
||||
4. `pnpm --filter @tessera/web test` — erwartet >= 68 Dateien, >= 480 Tests, 0 failed.
|
||||
5. `pnpm --filter @tessera/api test` — erwartet 72 Dateien, >= 1137 Tests, 0 failed.
|
||||
6. Byte-Scan von `apps/web/src/lib/safe-next.ts` — erwartet `CTRL_BYTES 0 []`.
|
||||
7. `git status --porcelain -- apps packages` — erwartet genau die 13 Dateien aus
|
||||
`files_modified`, keine weitere. Schlaegt das fehl, wurde formatiert oder
|
||||
umgeraeumt, was D-05 verbietet. (`.planning/` ist hier bewusst
|
||||
ausgeklammert: Plan und Zusammenfassung gehoeren dazu.)
|
||||
8. `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.
|
||||
|
||||
</verification>
|
||||
|
||||
<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.
|
||||
- `sanitizeNextPath` weist 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.ts` enthaelt 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 lint` bleibt 5/5 bei 0 Befunden der Stufe error; die Warnungszahl faellt
|
||||
von 445 auf 433 und waechst an keiner anderen Stelle (D-06).
|
||||
- `noArrayIndexKey` und `noNonNullAssertion` wurden nicht angefasst — sie gehoeren
|
||||
in den Folgevorgang. `noUselessSwitchCase` bleibt der dokumentierte
|
||||
Nicht-Fix aus 260921-bi2.
|
||||
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
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.
|
||||
</output>
|
||||
Reference in New Issue
Block a user