Aus der Plattform-Debatte vom 2026-09-01. Alle Befunde sind verifiziert, nicht vermutet —
jeder wurde in dieser Sitzung reproduziert. Sie gehören zusammen, weil sie denselben Codepfad
betreffen und weil sie zusammen Voraussetzung dafür sind, dass #19 überhaupt exponiert werden
darf.
1. ReDoS über --regex (der teuerste Befund)
tools/chemenu/search/ripgrep.py:147 gibt nutzergesteuerten Regex an Pythons
Backtracking-Engine:
Reproduziert: (\w+\s?)+$ gegen einen 114-Zeichen-Satz aus echtem Korpusinhalt terminiert in
8 Sekunden nicht. Ein deterministisch scheiterndes Muster (^(\w+\s+)+$) braucht 0,2 ms — der
Unterschied ist nicht der Haystack, sondern das Muster.
Amplifikation: build_hit ruft _contains pro Treffer auf Titel und Summary. Ein trivialer
Regex wie \w matcht jede Seite, also ~2 Suchen pro Seite, jede unbegrenzt.
Fix: den query.regex-Zweig in _contains löschen.rg entscheidet Regex ohnehin mit einer
linearen Engine (build_argv), der Python-Zweig ist Doppelarbeit mit exponentiellem
Worst Case. Zwei Zeilen plus ein Test.
Kein timeout=. Ein breiter Regex kostet in rg gemessen 1,4 s statt 0,006 s im
Fixed-String-Fall; ein hängender Aufruf blockiert unbegrenzt und puffert vollständig in den Heap.
Fix:timeout= setzen, subprocess.TimeoutExpired in den vorhandenen RipgrepFailed-Pfad übersetzen.
3. yaml.safe_load ohne Alias-Budget
tools/chemenu/frontmatter_io.py:37. Reproduziert: 322 Byte YAML mit verschachtelten Aliases
werden zu 797.161 logischen Knoten beim Traversal, Wachstum 3ⁿ bei konstanter Parse-Zeit. Ein
Lauf mit Tiefe 15/18 lief in ein 60-Sekunden-Timeout.
Heute nicht erreichbar: kb/ committet der Operator selbst, und der Anfragesteller liefert
Suchtext, kein YAML. Der Befund ist eine Parser-Eigenschaft, kein offener Pfad — er wird einer,
sobald fremdes YAML den Parser erreicht (#32).
Fix: Größen- und Tiefenlimit vor safe_load im Lesepfad; für den Quarantänepfad aus #32
zusätzlich strikt statt still auf {} zurückfallen (siehe Punkt 5).
4. CSafeLoader liegt ungenutzt herum
libyaml ist verfügbar. Gemessen über denselben Korpus: SafeLoader 246 ms, CSafeLoader
82 ms — Faktor 3, für eine Änderung an einer Stelle.
Das ist kein Mikro-Tuning: der Korpus-Parse ist mit 274 ms von 593 ms der größte Einzelposten
eines search-Aufrufs und skaliert linear mit der Korpusgröße. Bei einem Korpus in der
Größenordnung „wenige Tausend Seiten" ist das der Unterschied zwischen erträglich und nicht.
Fix:CSafeLoader benutzen, wo verfügbar, mit Fallback auf SafeLoader. Achtung: das
Alias-Problem aus Punkt 3 bleibt bestehen, die Limits sind unabhängig davon nötig.
5. Stiller Frontmatter-Verlust
tools/chemenu/frontmatter_io.py:37-41: kaputtes YAML wird zu {}. Eine Seite mit gebrochenem
Frontmatter verliert damit confidence/kind und rutscht durch jedes --field-Prädikat
hindurch. lint sieht es, die Suche nicht.
Fix: entscheiden, ob der Lesepfad das melden statt schlucken soll. Mindestens: der
Quarantäne-/Ingest-Pfad muss strikt sein.
6. Korpus-Reparse pro Aufruf
tools/chemenu/commands/search.py:199 → load_pages_by_path() liest und parst alle Seiten
bei jedem Aufruf: gemessen 274 ms für 176 Seiten. Ein Ein-Byte-Request kostet O(Korpus).
Fix: Cache, invalidiert am git-Commit-SHA des Checkouts. Derselbe SHA ist der
Antwort-Stempel aus #19 — ein Schlüssel für beides.
Was das zusammen bewirkt
Von 593 ms bleiben nach Punkt 4 und 6 plus dem residenten Prozess aus #19 (Importkosten einmal
statt pro Aufruf: −262 ms) grob 100 ms. Ohne Sprachwechsel.
Abgrenzung
Authentifizierung ist nicht Teil dieses Issues. Sie wird laut Betreiberentscheidung vom
2026-09-01 als Traefik-Middleware abgebildet; nicht sauber authentifizierte Zugriffe erreichen
den Python-Prozess nicht. Das entschärft Punkt 1 und 2 gegenüber anonymen Angreifern, hebt sie
aber nicht auf: ein authentifizierter Konsument kann dieselben Anfragen stellen, und ein
Ressourcenlimit gegen versehentliche Selbstbeschädigung bleibt richtig.
Akzeptanzkriterien
_contains gibt keinen nutzergesteuerten Regex mehr an re; Test mit (\w+\s?)+$ als Regression
subprocess.run in ripgrep.py hat ein timeout=, Überschreitung wird als RipgrepFailed gemeldet
Größen-/Tiefenlimit vor safe_load; Test mit dem 322-Byte-Alias-Dokument als Regression
CSafeLoader genutzt, mit Fallback; Verhalten unverändert (Round-Trip-Tests grün)
Entscheidung zu Punkt 5 getroffen und dokumentiert
Korpus-Cache am Commit-SHA; Messung vorher/nachher im Issue vermerkt
Version-Bump --patch, Changelog-Eintrag
Aus der Plattform-Debatte vom 2026-09-01. Alle Befunde sind **verifiziert**, nicht vermutet —
jeder wurde in dieser Sitzung reproduziert. Sie gehören zusammen, weil sie denselben Codepfad
betreffen und weil sie zusammen Voraussetzung dafür sind, dass #19 überhaupt exponiert werden
darf.
## 1. ReDoS über `--regex` (der teuerste Befund)
`tools/chemenu/search/ripgrep.py:147` gibt nutzergesteuerten Regex an Pythons
Backtracking-Engine:
```python
return re.search(query.text, haystack, re.IGNORECASE) is not None
```
Reproduziert: `(\w+\s?)+$` gegen einen 114-Zeichen-Satz aus echtem Korpusinhalt terminiert in
8 Sekunden nicht. Ein deterministisch scheiterndes Muster (`^(\w+\s+)+$`) braucht 0,2 ms — der
Unterschied ist nicht der Haystack, sondern das Muster.
Amplifikation: `build_hit` ruft `_contains` pro Treffer auf Titel **und** Summary. Ein trivialer
Regex wie `\w` matcht jede Seite, also ~2 Suchen pro Seite, jede unbegrenzt.
**Fix: den `query.regex`-Zweig in `_contains` löschen.** `rg` entscheidet Regex ohnehin mit einer
linearen Engine (`build_argv`), der Python-Zweig ist Doppelarbeit mit exponentiellem
Worst Case. Zwei Zeilen plus ein Test.
## 2. Kein Timeout auf dem Subprozess
`tools/chemenu/search/ripgrep.py:102`:
```python
proc = subprocess.run(argv, capture_output=True, text=True, check=False)
```
Kein `timeout=`. Ein breiter Regex kostet in `rg` gemessen 1,4 s statt 0,006 s im
Fixed-String-Fall; ein hängender Aufruf blockiert unbegrenzt und puffert vollständig in den Heap.
**Fix:** `timeout=` setzen, `subprocess.TimeoutExpired` in den vorhandenen
`RipgrepFailed`-Pfad übersetzen.
## 3. `yaml.safe_load` ohne Alias-Budget
`tools/chemenu/frontmatter_io.py:37`. Reproduziert: 322 Byte YAML mit verschachtelten Aliases
werden zu 797.161 logischen Knoten beim Traversal, Wachstum 3ⁿ bei konstanter Parse-Zeit. Ein
Lauf mit Tiefe 15/18 lief in ein 60-Sekunden-Timeout.
Heute **nicht erreichbar**: `kb/` committet der Operator selbst, und der Anfragesteller liefert
Suchtext, kein YAML. Der Befund ist eine Parser-Eigenschaft, kein offener Pfad — er wird einer,
sobald fremdes YAML den Parser erreicht (#32).
**Fix:** Größen- und Tiefenlimit vor `safe_load` im Lesepfad; für den Quarantänepfad aus #32
zusätzlich strikt statt still auf `{}` zurückfallen (siehe Punkt 5).
## 4. `CSafeLoader` liegt ungenutzt herum
`libyaml` ist verfügbar. Gemessen über denselben Korpus: `SafeLoader` 246 ms, **`CSafeLoader`
82 ms** — Faktor 3, für eine Änderung an einer Stelle.
Das ist kein Mikro-Tuning: der Korpus-Parse ist mit 274 ms von 593 ms der größte Einzelposten
eines `search`-Aufrufs und skaliert linear mit der Korpusgröße. Bei einem Korpus in der
Größenordnung „wenige Tausend Seiten" ist das der Unterschied zwischen erträglich und nicht.
**Fix:** `CSafeLoader` benutzen, wo verfügbar, mit Fallback auf `SafeLoader`. Achtung: das
Alias-Problem aus Punkt 3 bleibt bestehen, die Limits sind unabhängig davon nötig.
## 5. Stiller Frontmatter-Verlust
`tools/chemenu/frontmatter_io.py:37-41`: kaputtes YAML wird zu `{}`. Eine Seite mit gebrochenem
Frontmatter verliert damit `confidence`/`kind` und rutscht durch jedes `--field`-Prädikat
hindurch. `lint` sieht es, die Suche nicht.
**Fix:** entscheiden, ob der Lesepfad das melden statt schlucken soll. Mindestens: der
Quarantäne-/Ingest-Pfad muss strikt sein.
## 6. Korpus-Reparse pro Aufruf
`tools/chemenu/commands/search.py:199` → `load_pages_by_path()` liest und parst **alle** Seiten
bei jedem Aufruf: gemessen 274 ms für 176 Seiten. Ein Ein-Byte-Request kostet O(Korpus).
**Fix:** Cache, invalidiert am git-Commit-SHA des Checkouts. Derselbe SHA ist der
Antwort-Stempel aus #19 — ein Schlüssel für beides.
## Was das zusammen bewirkt
Von 593 ms bleiben nach Punkt 4 und 6 plus dem residenten Prozess aus #19 (Importkosten einmal
statt pro Aufruf: −262 ms) grob **100 ms**. Ohne Sprachwechsel.
## Abgrenzung
Authentifizierung ist **nicht** Teil dieses Issues. Sie wird laut Betreiberentscheidung vom
2026-09-01 als Traefik-Middleware abgebildet; nicht sauber authentifizierte Zugriffe erreichen
den Python-Prozess nicht. Das entschärft Punkt 1 und 2 gegenüber anonymen Angreifern, hebt sie
aber nicht auf: ein authentifizierter Konsument kann dieselben Anfragen stellen, und ein
Ressourcenlimit gegen versehentliche Selbstbeschädigung bleibt richtig.
## Akzeptanzkriterien
- [ ] `_contains` gibt keinen nutzergesteuerten Regex mehr an `re`; Test mit `(\w+\s?)+$` als Regression
- [ ] `subprocess.run` in `ripgrep.py` hat ein `timeout=`, Überschreitung wird als `RipgrepFailed` gemeldet
- [ ] Größen-/Tiefenlimit vor `safe_load`; Test mit dem 322-Byte-Alias-Dokument als Regression
- [ ] `CSafeLoader` genutzt, mit Fallback; Verhalten unverändert (Round-Trip-Tests grün)
- [ ] Entscheidung zu Punkt 5 getroffen und dokumentiert
- [ ] Korpus-Cache am Commit-SHA; Messung vorher/nachher im Issue vermerkt
- [ ] Version-Bump `--patch`, Changelog-Eintrag
Teil der Sequenz in #36 (Master: Weg zum MCP-Leseserver) — dort Schritt 1 von 3.
Vor #31, weil beide dieselben Dateien anfassen: dieses Issue oberflächlich
(search/ripgrep.py, frontmatter_io.py), #31 strukturell. Andersherum entsteht ein Konflikt
umsonst.
Teil der Sequenz in **#36** (Master: Weg zum MCP-Leseserver) — dort Schritt 1 von 3.
Vor #31, weil beide dieselben Dateien anfassen: dieses Issue oberflächlich
(`search/ripgrep.py`, `frontmatter_io.py`), #31 strukturell. Andersherum entsteht ein Konflikt
umsonst.
Umgesetzt in 2.2.4. Alle sechs Punkte, jeder mit einer Regression.
1. ReDoS. Der query.regex-Zweig in _contains ist gelöscht, nicht begrenzt. rg hat
das Muster mit einer linearen Engine längst angewendet, wenn die Funktion überhaupt läuft.
Verloren geht nur der zusätzliche Titel-/Summary-Bonus für nicht-literale Muster — und Summary
wie H1 sind selbst Zeilen in der Datei, die rg zählt. Ein überwiegend literales Muster
(longhorn) bekommt seinen Bonus weiterhin über den Substring-Pfad. Regression prüft beides:
Laufzeit (< 0,5 s bei (\w+\s?)+$) und Ergebnis, weil eine Zeitschranke allein auch mit
einer bloß schnelleren Engine grün würde.
2. Timeout.RIPGREP_TIMEOUT_SECONDS = 30, TimeoutExpired → RipgrepFailed. Kein
Performance-Budget, ein Hänger-Abbruch.
3. YAML-Aliases: verweigert, nicht budgetiert. Frontmatter hat keine legitime Verwendung
dafür. Die Prüfung läuft auf dem Event-Strom (yaml.parse), der nichts auflöst — kostet
O(Text) und löst nie aus, wonach sie fragt. * ist in jedem Alias-Knoten notwendig, seine
Abwesenheit beweist Abwesenheit ohne Parse; das ist der Weg jeder echten Seite. Dazu 64 KiB
Größenlimit und ein Abfangen von RecursionError (PyYAML komponiert rekursiv, tiefe
Verschachtelung ist kein YAMLError).
Zur Zahl im Issue-Text: mein Regressionsdokument ist etwas kleiner geraten, 267 Byte → 672.603
Knoten beim Traversal, Parse in 0,2 ms, Wachstum 9ⁿ. Dieselbe Eigenschaft, eigene Messung.
4. CSafeLoader. Gemessen über diesen Korpus (176 Seiten, best of 5): 265 ms → 54 ms.
5. Entscheidung getroffen: der Lesepfad meldet. Kaputtes YAML wird weiterhin zu {} —
Massenoperationen dürfen an einer Seite nicht scheitern —, aber der Grund wird mitgeführt
(Page.frontmatter_error) und ausgegeben: search --json trägt immer eine unreadable-Liste
aus {path, reason}, die Tabellenform schreibt dieselben Zeilen nach stderr. Nötig, weil so eine
Seite weder confidence noch kind hat und damit aus jedem positiven --field-Prädikat fällt —
ausgerechnet aus der Low-Confidence-Suche, die Seiten in genau diesem Zustand finden soll — und
dabei aussieht wie eine Seite, die nicht gematcht hat. Ein leerer Frontmatter-Block gilt nicht als
Befund; eine Seite ganz ohne Block schon. Für den Quarantänepfad aus #32 steht read_page_strict() bereit: unlesbar muss dort das Dokument stoppen, nicht es leeren.
6. Korpus-Cache in chemenu/corpus_cache.py, als Objekt, das ein Aufrufer hält — kein
Modul-Dict, das sich hinter allen einschaltet. Die CLI hält keins (ein Aufruf pro Prozess, nichts
wiederzuverwenden), der residente Prozess aus #19 hält eins. Entscheidend ist nicht Geschwindigkeit,
sondern dass nichts veraltet: ein schmutziger Arbeitsbaum wird nie gecacht, sonst bekäme eine
Sitzung, die eine Seite schreibt und danach sucht, die Analyse von vor dem Schreiben — bei
unverändertem SHA. Kann git nicht antworten, gilt der Baum als schmutzig.
Messung vorher/nachher, wie gefordert:
vorher
nachher
Korpus-Parse (176 Seiten, best of 5)
265 ms
54 ms
wikitool search end-to-end (best of 5)
593 ms
347 ms
Die verbleibenden ~262 ms sind Modulimport und entfallen erst im residenten Prozess (#19).
Nebenertrag:read_page() und frontmatter_error() liefen getrennt durch safe_load — so
konnte der permissive Weg zu {} degradieren aus einem Grund, den der strikte Weg anders
beschrieb, und jeder Aufrufer, der beide Antworten wollte, las die Datei zweimal. Beide gehen
jetzt durch einen Parser; ein Test hält sie gegeneinander.
Erhalten geblieben ist Befund 10: kein shell=True, --fixed-strings als Default, ---Terminator. Der Modul-Docstring führt sie jetzt als drei tragende Zusagen statt zwei — die
dritte ist, dass nutzergesteuerte Muster ausschließlich rg sieht.
Umgesetzt in **2.2.4**. Alle sechs Punkte, jeder mit einer Regression.
**1. ReDoS.** Der `query.regex`-Zweig in `_contains` ist **gelöscht**, nicht begrenzt. `rg` hat
das Muster mit einer linearen Engine längst angewendet, wenn die Funktion überhaupt läuft.
Verloren geht nur der zusätzliche Titel-/Summary-Bonus für nicht-literale Muster — und Summary
wie H1 sind selbst Zeilen in der Datei, die `rg` zählt. Ein überwiegend literales Muster
(`longhorn`) bekommt seinen Bonus weiterhin über den Substring-Pfad. Regression prüft beides:
Laufzeit (`< 0,5 s` bei `(\w+\s?)+$`) **und** Ergebnis, weil eine Zeitschranke allein auch mit
einer bloß schnelleren Engine grün würde.
**2. Timeout.** `RIPGREP_TIMEOUT_SECONDS = 30`, `TimeoutExpired` → `RipgrepFailed`. Kein
Performance-Budget, ein Hänger-Abbruch.
**3. YAML-Aliases: verweigert, nicht budgetiert.** Frontmatter hat keine legitime Verwendung
dafür. Die Prüfung läuft auf dem *Event*-Strom (`yaml.parse`), der nichts auflöst — kostet
O(Text) und löst nie aus, wonach sie fragt. `*` ist in jedem Alias-Knoten notwendig, seine
Abwesenheit beweist Abwesenheit ohne Parse; das ist der Weg jeder echten Seite. Dazu 64 KiB
Größenlimit und ein Abfangen von `RecursionError` (PyYAML komponiert rekursiv, tiefe
Verschachtelung ist kein `YAMLError`).
Zur Zahl im Issue-Text: mein Regressionsdokument ist etwas kleiner geraten, **267 Byte → 672.603
Knoten beim Traversal, Parse in 0,2 ms**, Wachstum 9ⁿ. Dieselbe Eigenschaft, eigene Messung.
**4. `CSafeLoader`.** Gemessen über diesen Korpus (176 Seiten, best of 5): **265 ms → 54 ms**.
**5. Entscheidung getroffen: der Lesepfad meldet.** Kaputtes YAML wird weiterhin zu `{}` —
Massenoperationen dürfen an einer Seite nicht scheitern —, aber der Grund wird mitgeführt
(`Page.frontmatter_error`) und ausgegeben: `search --json` trägt immer eine `unreadable`-Liste
aus `{path, reason}`, die Tabellenform schreibt dieselben Zeilen nach stderr. Nötig, weil so eine
Seite weder `confidence` noch `kind` hat und damit aus jedem positiven `--field`-Prädikat fällt —
ausgerechnet aus der Low-Confidence-Suche, die Seiten in genau diesem Zustand finden soll — und
dabei aussieht wie eine Seite, die nicht gematcht hat. Ein leerer Frontmatter-Block gilt nicht als
Befund; eine Seite ganz ohne Block schon. Für den Quarantänepfad aus #32 steht
`read_page_strict()` bereit: unlesbar muss dort das Dokument stoppen, nicht es leeren.
**6. Korpus-Cache** in `chemenu/corpus_cache.py`, als Objekt, das ein Aufrufer *hält* — kein
Modul-Dict, das sich hinter allen einschaltet. Die CLI hält keins (ein Aufruf pro Prozess, nichts
wiederzuverwenden), der residente Prozess aus #19 hält eins. Entscheidend ist nicht Geschwindigkeit,
sondern dass nichts veraltet: **ein schmutziger Arbeitsbaum wird nie gecacht**, sonst bekäme eine
Sitzung, die eine Seite schreibt und danach sucht, die Analyse von vor dem Schreiben — bei
unverändertem SHA. Kann git nicht antworten, gilt der Baum als schmutzig.
**Messung vorher/nachher**, wie gefordert:
| | vorher | nachher |
|---|---|---|
| Korpus-Parse (176 Seiten, best of 5) | 265 ms | **54 ms** |
| `wikitool search` end-to-end (best of 5) | 593 ms | **347 ms** |
Die verbleibenden ~262 ms sind Modulimport und entfallen erst im residenten Prozess (#19).
**Nebenertrag:** `read_page()` und `frontmatter_error()` liefen getrennt durch `safe_load` — so
konnte der permissive Weg zu `{}` degradieren aus einem Grund, den der strikte Weg anders
beschrieb, und jeder Aufrufer, der beide Antworten wollte, las die Datei zweimal. Beide gehen
jetzt durch einen Parser; ein Test hält sie gegeneinander.
**Erhalten geblieben** ist Befund 10: kein `shell=True`, `--fixed-strings` als Default,
`--`-Terminator. Der Modul-Docstring führt sie jetzt als drei tragende Zusagen statt zwei — die
dritte ist, dass nutzergesteuerte Muster ausschließlich `rg` sieht.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Aus der Plattform-Debatte vom 2026-09-01. Alle Befunde sind verifiziert, nicht vermutet —
jeder wurde in dieser Sitzung reproduziert. Sie gehören zusammen, weil sie denselben Codepfad
betreffen und weil sie zusammen Voraussetzung dafür sind, dass #19 überhaupt exponiert werden
darf.
1. ReDoS über
--regex(der teuerste Befund)tools/chemenu/search/ripgrep.py:147gibt nutzergesteuerten Regex an PythonsBacktracking-Engine:
Reproduziert:
(\w+\s?)+$gegen einen 114-Zeichen-Satz aus echtem Korpusinhalt terminiert in8 Sekunden nicht. Ein deterministisch scheiterndes Muster (
^(\w+\s+)+$) braucht 0,2 ms — derUnterschied ist nicht der Haystack, sondern das Muster.
Amplifikation:
build_hitruft_containspro Treffer auf Titel und Summary. Ein trivialerRegex wie
\wmatcht jede Seite, also ~2 Suchen pro Seite, jede unbegrenzt.Fix: den
query.regex-Zweig in_containslöschen.rgentscheidet Regex ohnehin mit einerlinearen Engine (
build_argv), der Python-Zweig ist Doppelarbeit mit exponentiellemWorst Case. Zwei Zeilen plus ein Test.
2. Kein Timeout auf dem Subprozess
tools/chemenu/search/ripgrep.py:102:Kein
timeout=. Ein breiter Regex kostet inrggemessen 1,4 s statt 0,006 s imFixed-String-Fall; ein hängender Aufruf blockiert unbegrenzt und puffert vollständig in den Heap.
Fix:
timeout=setzen,subprocess.TimeoutExpiredin den vorhandenenRipgrepFailed-Pfad übersetzen.3.
yaml.safe_loadohne Alias-Budgettools/chemenu/frontmatter_io.py:37. Reproduziert: 322 Byte YAML mit verschachtelten Aliaseswerden zu 797.161 logischen Knoten beim Traversal, Wachstum 3ⁿ bei konstanter Parse-Zeit. Ein
Lauf mit Tiefe 15/18 lief in ein 60-Sekunden-Timeout.
Heute nicht erreichbar:
kb/committet der Operator selbst, und der Anfragesteller liefertSuchtext, kein YAML. Der Befund ist eine Parser-Eigenschaft, kein offener Pfad — er wird einer,
sobald fremdes YAML den Parser erreicht (#32).
Fix: Größen- und Tiefenlimit vor
safe_loadim Lesepfad; für den Quarantänepfad aus #32zusätzlich strikt statt still auf
{}zurückfallen (siehe Punkt 5).4.
CSafeLoaderliegt ungenutzt herumlibyamlist verfügbar. Gemessen über denselben Korpus:SafeLoader246 ms,CSafeLoader82 ms — Faktor 3, für eine Änderung an einer Stelle.
Das ist kein Mikro-Tuning: der Korpus-Parse ist mit 274 ms von 593 ms der größte Einzelposten
eines
search-Aufrufs und skaliert linear mit der Korpusgröße. Bei einem Korpus in derGrößenordnung „wenige Tausend Seiten" ist das der Unterschied zwischen erträglich und nicht.
Fix:
CSafeLoaderbenutzen, wo verfügbar, mit Fallback aufSafeLoader. Achtung: dasAlias-Problem aus Punkt 3 bleibt bestehen, die Limits sind unabhängig davon nötig.
5. Stiller Frontmatter-Verlust
tools/chemenu/frontmatter_io.py:37-41: kaputtes YAML wird zu{}. Eine Seite mit gebrochenemFrontmatter verliert damit
confidence/kindund rutscht durch jedes--field-Prädikathindurch.
lintsieht es, die Suche nicht.Fix: entscheiden, ob der Lesepfad das melden statt schlucken soll. Mindestens: der
Quarantäne-/Ingest-Pfad muss strikt sein.
6. Korpus-Reparse pro Aufruf
tools/chemenu/commands/search.py:199→load_pages_by_path()liest und parst alle Seitenbei jedem Aufruf: gemessen 274 ms für 176 Seiten. Ein Ein-Byte-Request kostet O(Korpus).
Fix: Cache, invalidiert am git-Commit-SHA des Checkouts. Derselbe SHA ist der
Antwort-Stempel aus #19 — ein Schlüssel für beides.
Was das zusammen bewirkt
Von 593 ms bleiben nach Punkt 4 und 6 plus dem residenten Prozess aus #19 (Importkosten einmal
statt pro Aufruf: −262 ms) grob 100 ms. Ohne Sprachwechsel.
Abgrenzung
Authentifizierung ist nicht Teil dieses Issues. Sie wird laut Betreiberentscheidung vom
2026-09-01 als Traefik-Middleware abgebildet; nicht sauber authentifizierte Zugriffe erreichen
den Python-Prozess nicht. Das entschärft Punkt 1 und 2 gegenüber anonymen Angreifern, hebt sie
aber nicht auf: ein authentifizierter Konsument kann dieselben Anfragen stellen, und ein
Ressourcenlimit gegen versehentliche Selbstbeschädigung bleibt richtig.
Akzeptanzkriterien
_containsgibt keinen nutzergesteuerten Regex mehr anre; Test mit(\w+\s?)+$als Regressionsubprocess.runinripgrep.pyhat eintimeout=, Überschreitung wird alsRipgrepFailedgemeldetsafe_load; Test mit dem 322-Byte-Alias-Dokument als RegressionCSafeLoadergenutzt, mit Fallback; Verhalten unverändert (Round-Trip-Tests grün)--patch, Changelog-EintragTeil der Sequenz in #36 (Master: Weg zum MCP-Leseserver) — dort Schritt 1 von 3.
Vor #31, weil beide dieselben Dateien anfassen: dieses Issue oberflächlich
(
search/ripgrep.py,frontmatter_io.py), #31 strukturell. Andersherum entsteht ein Konfliktumsonst.
Umgesetzt in 2.2.4. Alle sechs Punkte, jeder mit einer Regression.
1. ReDoS. Der
query.regex-Zweig in_containsist gelöscht, nicht begrenzt.rghatdas Muster mit einer linearen Engine längst angewendet, wenn die Funktion überhaupt läuft.
Verloren geht nur der zusätzliche Titel-/Summary-Bonus für nicht-literale Muster — und Summary
wie H1 sind selbst Zeilen in der Datei, die
rgzählt. Ein überwiegend literales Muster(
longhorn) bekommt seinen Bonus weiterhin über den Substring-Pfad. Regression prüft beides:Laufzeit (
< 0,5 sbei(\w+\s?)+$) und Ergebnis, weil eine Zeitschranke allein auch miteiner bloß schnelleren Engine grün würde.
2. Timeout.
RIPGREP_TIMEOUT_SECONDS = 30,TimeoutExpired→RipgrepFailed. KeinPerformance-Budget, ein Hänger-Abbruch.
3. YAML-Aliases: verweigert, nicht budgetiert. Frontmatter hat keine legitime Verwendung
dafür. Die Prüfung läuft auf dem Event-Strom (
yaml.parse), der nichts auflöst — kostetO(Text) und löst nie aus, wonach sie fragt.
*ist in jedem Alias-Knoten notwendig, seineAbwesenheit beweist Abwesenheit ohne Parse; das ist der Weg jeder echten Seite. Dazu 64 KiB
Größenlimit und ein Abfangen von
RecursionError(PyYAML komponiert rekursiv, tiefeVerschachtelung ist kein
YAMLError).Zur Zahl im Issue-Text: mein Regressionsdokument ist etwas kleiner geraten, 267 Byte → 672.603
Knoten beim Traversal, Parse in 0,2 ms, Wachstum 9ⁿ. Dieselbe Eigenschaft, eigene Messung.
4.
CSafeLoader. Gemessen über diesen Korpus (176 Seiten, best of 5): 265 ms → 54 ms.5. Entscheidung getroffen: der Lesepfad meldet. Kaputtes YAML wird weiterhin zu
{}—Massenoperationen dürfen an einer Seite nicht scheitern —, aber der Grund wird mitgeführt
(
Page.frontmatter_error) und ausgegeben:search --jsonträgt immer eineunreadable-Listeaus
{path, reason}, die Tabellenform schreibt dieselben Zeilen nach stderr. Nötig, weil so eineSeite weder
confidencenochkindhat und damit aus jedem positiven--field-Prädikat fällt —ausgerechnet aus der Low-Confidence-Suche, die Seiten in genau diesem Zustand finden soll — und
dabei aussieht wie eine Seite, die nicht gematcht hat. Ein leerer Frontmatter-Block gilt nicht als
Befund; eine Seite ganz ohne Block schon. Für den Quarantänepfad aus #32 steht
read_page_strict()bereit: unlesbar muss dort das Dokument stoppen, nicht es leeren.6. Korpus-Cache in
chemenu/corpus_cache.py, als Objekt, das ein Aufrufer hält — keinModul-Dict, das sich hinter allen einschaltet. Die CLI hält keins (ein Aufruf pro Prozess, nichts
wiederzuverwenden), der residente Prozess aus #19 hält eins. Entscheidend ist nicht Geschwindigkeit,
sondern dass nichts veraltet: ein schmutziger Arbeitsbaum wird nie gecacht, sonst bekäme eine
Sitzung, die eine Seite schreibt und danach sucht, die Analyse von vor dem Schreiben — bei
unverändertem SHA. Kann git nicht antworten, gilt der Baum als schmutzig.
Messung vorher/nachher, wie gefordert:
wikitool searchend-to-end (best of 5)Die verbleibenden ~262 ms sind Modulimport und entfallen erst im residenten Prozess (#19).
Nebenertrag:
read_page()undfrontmatter_error()liefen getrennt durchsafe_load— sokonnte der permissive Weg zu
{}degradieren aus einem Grund, den der strikte Weg andersbeschrieb, und jeder Aufrufer, der beide Antworten wollte, las die Datei zweimal. Beide gehen
jetzt durch einen Parser; ein Test hält sie gegeneinander.
Erhalten geblieben ist Befund 10: kein
shell=True,--fixed-stringsals Default,---Terminator. Der Modul-Docstring führt sie jetzt als drei tragende Zusagen statt zwei — diedritte ist, dass nutzergesteuerte Muster ausschließlich
rgsieht.