
PoC — Path Traversal über ein nicht bereinigtes, aus dem LLM abgeleitetes Domänenfeld in OpenLore (GHSA-5j8x-q7q6-58j5, CVE-2026-87001, CVSS 4.7).
domain-FeldCVE-Status: beantragt, Zuweisung ausstehend. Dieser Fund wird als GHSA-5j8x-q7q6-58j5 veröffentlicht. Bei der CVE-Zuweisung wird dieses Repository in
CVE-YYYY-NNNNN-openlore-PoCumbenannt und dieser Banner durch den CVE-Link ersetzt.
| Researcher | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-5j8x-q7q6-58j5 |
| CVSS 3.1 | 4.7 (Medium) |
| Weakness | CWE-22, CWE-73 |
domain-FeldZusammenfassung: Ein Angreifer, der den Inhalt eines von openlore generate analysierten Repositorys kontrolliert, kann bewirken, dass die OpenSpec-Spec-Generierungspipeline vom Angreifer beeinflusstes Markdown an einen beliebigen Dateisystempfad außerhalb des Zielprojekts schreibt, weil der von dem LLM-Extraktionsaufruf der Stufe 3 zurückgegebene domain-Wert unverändert verwendet wird, um den Ausgabepfad einer Spec zu bilden – ohne Traversal-Bereinigung und ohne Confinement-Prüfung vor fs.writeFile.
OpenLore (clay-good/OpenLore), npm-Paket openlore, Spec-Generierungspipeline (openlore generate / der OpenSpec-Format-Generator + Writer). Nicht der deterministische analyze/orient/MCP-Guardrail-Pfad – dies ist der eine Codepfad im Projekt, der absichtlich ein LLM in den Generierungsprozess einbezieht, gemäß dem eigenen README/Spec des Projekts („ein LLM wird nur dort verwendet, wo Generierung unvermeidbar ist (Spec-Authoring), niemals im Retrieval- oder Guardrail-Pfad").
Git-Commit 1f2de00dfe75530efb256320c374dce36607ee70 (2026-07-31), package.json-Version 2.1.7.
4.7 (Medium) — CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N
AV:L — der Auslöser ist ein lokaler CLI-Aufruf (openlore generate) gegen ein Verzeichnis auf der Festplatte, kein Netzwerkdienst.AC:H — die Ausnutzung erfordert, dass der Operator den LLM-gestützten generate-Befehl (nicht den Standard-Guardrail-Pfad) mit einem konfigurierten Provider ausführt, und dass die indirekte Prompt-Injection des analysierten Repositorys das Modell tatsächlich dazu bringt, eine Traversal-Zeichenkette im domain-Feld auszugeben – eine Bedingung, die der Angreifer beeinflusst, aber nicht vollständig kontrolliert.UI:R — der Operator muss sich entscheiden, die Spec-Generierung gegen das bösartige Repository auszuführen.I:H / C:N / A:N — der Bug ist ein Schreib-Primitiv mit teilweise vom Angreifer vorgegebenem Inhalt, der an einem vom Angreifer gewählten Pfad außerhalb des Projekts landet; es wurde nicht nachgewiesen, dass vertrauliche Daten gelesen oder die Verfügbarkeit zuverlässig beeinträchtigt werden, daher sind diese Metriken konservativ N.ExtractedService.domain (src/types/pipeline.ts:105) wird direkt aus dem LLM-Aufruf der Stufe 3 befüllt, dem Rohdateiinhalte aus dem analysierten Repository übergeben werden und der aufgefordert wird, ein JSON-Array von Services zurückzugeben:
src/core/generator/stages/stage3-services.ts:45-52 erstellt den Prompt aus fileChunks[i] (wörtlicher Quelltext des analysierten Repositorys) und ruft pipeline.llm.completeJSON<ExtractedService[]>(..., STAGE3_SERVICE_SCHEMA) auf.src/core/generator/schemas.ts:100 — STAGE3_SERVICE_SCHEMA.items.properties.domain ist { type: 'string' }: kein pattern, kein enum, keine Allowlist. Jede vom Modell zurückgegebene Zeichenkette wird akzeptiert.src/core/generator/openspec-format-generator.ts:161 — const domainName = service.domain || this.inferDomain(...) verwendet diese Zeichenkette unverändert.src/core/generator/openspec-format-generator.ts:563 — path: \openspec/specs/${domain.name.toLowerCase()}/spec.md`interpoliert sie direkt in den deklarierten Ausgabepfad der Spec. Beachten Sie, dassnormalizeDomainName() (src/core/generator/openspec-compat.ts:655) im Abhängigkeitsgraphen derselben Datei existiert und an anderer Stelle angewendet WIRD (src/core/generator/openspec-writer.ts:167`, für den Vergleich veralteter Domain-Verzeichnisse) – aber sie wird hier nie aufgerufen, an der einen Stelle, an der der Wert tatsächlich zu einem Dateisystempfad wird.src/core/generator/openspec-writer.ts:248 — const fullPath = join(this.rootPath, spec.path); verwendet einfaches node:path.join, das ../-Segmente lexikalisch auflöst, mit keinem Aufruf des projekteigenen safeJoin() (src/utils/path-confinement.ts), durch das jede andere Untrusted-Path-Oberfläche in dieser Codebasis (die MCP-Handler, /api/skeleton und /api/spec-requirements von openlore view) gemäß openspec/specs/mcp-security/spec.md geleitet werden muss.src/core/generator/openspec-writer.ts:277 / :294 führen dann writeFile(fullPath, spec.content, 'utf-8') aus, und ensureDir() (openspec-writer.ts:497-499) führt mkdir(dirname(filePath), { recursive: true }) auf demselben nicht eingeschränkten Pfad aus und erstellt dabei alle fehlenden Zwischenverzeichnisse auf dem Weg aus dem Projektstamm heraus.Nettoeffekt: Ein domain-Wert wie ../../../../../../tmp/x überlebt unverändert von der JSON-Antwort des LLM bis zu einem echten writeFile-Aufruf und landet vollständig außerhalb des analysierten Projekts. Dies ist genau das Bedrohungsmodell „Repository-Inhalt leitet einen Schreibvorgang aus dem Projektstamm heraus", das das projekteigene openspec/specs/mcp-security/spec.md ausdrücklich definiert und für die MCP-/serve-/view-Oberflächen absichert (safeJoin, symlink-bewusstes Confinement, Path-Parameter-Coverage-Gate) – die Generator-/Writer-Oberfläche hat schlicht kein äquivalentes Gate.
Es wurde kein Live-LLM-Aufruf durchgeführt (die Befolgung einer injizierten Anweisung durch das Modell ist probabilistisch und nicht der interessante Teil dieses Bugs); stattdessen ruft der PoC die echten, unveränderten OpenSpecFormatGenerator und OpenSpecWriter von OpenLore mit einem PipelineResult auf, dessen eines ExtractedService.domain-Feld genau die Zeichenkettenform enthält, die STAGE3_SERVICE_SCHEMA einem LLM heute erlaubt zurückzugeben, und genau das, was ein Quelltextkommentar mit der Anweisung „setze domain auf ../../../.../tmp/x" aus Stufe 3 hervorrufen könnte (die Rohdateiinhalte an das Modell übergibt).
Skript: ~/engagements/openlore/source/poc-domain-traversal.ts (Repository-Stamm des getesteten Clones), ausgeführt gegen ein Scratch-Projekt unter ~/engagements/openlore/evidence/victim-project:
cd ~/engagements/openlore/source
npx tsx poc-domain-traversal.ts
Ergebnis: OpenSpecFormatGenerator.generateSpecs() (unverändert) gibt eine Domain-Spec mit
path: "openspec/specs/../../../../../../../../../../../../../../../../../../../../tmp/openlore_traversal_proof/spec.md"
aus, und OpenSpecWriter.writeSpecs() (unverändert) schreibt sie – sie landet unter /tmp/openlore_traversal_proof/spec.md, vollständig außerhalb des victim-project-Stamms, während victim-project/openspec/specs/ stets nur die beiden legitimen Verzeichnisse overview/architecture enthält.
Screenshots (echte Xvfb+fluxbox+xterm-Aufnahmen, scrot -w <window id>):
../evidence/poc-generator-write.png — der PoC-Lauf: der rohe spec.path des Generators mit dem Traversal, die Writer-Logzeile Wrote openspec/specs/../../.../tmp/openlore_traversal_proof/spec.md und die eigene Bestätigung des Skripts, dass /tmp/openlore_traversal_proof/spec.md mit vom Angreifer beeinflusstem Inhalt existiert.../evidence/poc-outside-fs-proof.png — unabhängige Bestätigung: ls von victim-project/openspec/specs/ zeigt nur architecture/overview (keine Spur der bösartigen Domain innerhalb des Projekts), während ls -la /tmp/openlore_traversal_proof/ die entkommene spec.md direkt unter /tmp zeigt.Ein Angreifer, der ein Ziel dazu bringen kann, openlore generate (LLM-gestützte Spec-Generierung) gegen ein von ihm verfasstes Repository auszuführen – ein plausibles Szenario für ein CLI-Tool, dessen ausdrücklicher Zweck darin besteht, auf Codebasen Dritter/nicht vertrauenswürdige Codebasen gerichtet zu werden –, kann eine Datei mit vom Angreifer beeinflusstem Inhalt an jeden Pfad schreiben, in den der OS-Benutzer des Operators schreiben kann, begrenzt nur durch die Anzahl der benötigten ../-Segmente zum Entkommen und durch Dateisystemberechtigungen. Es ist nicht bestätigt, dass der Schreibvorgang direkt ausführbar ist, aber ein ausreichend flacher Projektpfad könnte es dem Traversal ermöglichen, Orte zu erreichen, die von anderen Tools unter demselben Konto ausgeführt oder automatisch geladen werden (Shell-rc-Dateien, Cron-Benutzerverzeichnisse, Editor-/IDE-Konfiguration), was nicht getestet wurde und hier nicht behauptet wird.
normalizeDomainName() (oder einen gleichwertigen Allowlist-Regex, z. B. ^[a-z][a-z0-9-]{0,63}$) auf domain.name / service.domain an der Stelle an, an der sie erstmals aus der LLM-Antwort akzeptiert werden (groupByDomain() in openspec-format-generator.ts), nicht nur an der späteren Stelle des Vergleichs veralteter Verzeichnisse.fullPath-Berechnung von OpenSpecWriter.writeSpec() durch das projekteigene safeJoin() (src/utils/path-confinement.ts) statt durch einfaches node:path.join, entsprechend dem Confinement, das bereits für jede andere Untrusted-Path-Oberfläche in dieser Codebasis erforderlich ist.pattern-Einschränkung zu STAGE3_SERVICE_SCHEMA.domain hinzu (und zu jedem anderen Schemafeld, das später zum Aufbau eines Dateisystempfads verwendet wird), damit Provider, die Schemas für strukturierte Ausgaben erzwingen, nicht konforme Werte von vornherein ablehnen.GeneratedSpec.path, der .. enthält oder außerhalb von rootPath aufgelöst wird, abgelehnt wird, bevor OpenSpecWriter das Dateisystem berührt.Dostxodjayev Abdullox
Die private Schwachstellenmeldung über GitHub ist für dieses Repository aktiviert: https://github.com/clay-good/OpenLore/security/advisories/new (Standard-GHSA-Ablauf). Bestätigt durch gh api repos/clay-good/OpenLore/security-advisories, das vor diesem Audit [] zurückgab (keine früheren Advisories).