
PoC — path traversal tramite campo dominio non sanificato derivato da LLM in OpenLore (GHSA-5j8x-q7q6-58j5, CVE-2026-87001, CVSS 4.7).
domain Derivato dall'LLM e Non SanificatoStato CVE: richiesto, in attesa di assegnazione. Questa scoperta è pubblicata come GHSA-5j8x-q7q6-58j5. All'assegnazione del CVE questo repository viene rinominato
CVE-YYYY-NNNNN-openlore-PoCe questo banner viene sostituito con il link al CVE.
| Ricercatore | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-5j8x-q7q6-58j5 |
| CVSS 3.1 | 4.7 (Medium) |
| Debolezza | CWE-22, CWE-73 |
domain Derivato dall'LLM e Non SanificatoSommario: Un attaccante che controlla il contenuto di un repository analizzato da openlore generate può causare la scrittura, da parte della pipeline di generazione delle spec OpenSpec, di Markdown influenzato dall'attaccante in un percorso arbitrario del filesystem al di fuori del progetto target, poiché il valore domain restituito dalla chiamata di estrazione LLM della Stage-3 viene utilizzato alla lettera per costruire il percorso di output di una spec, senza alcuna sanificazione del traversal e senza alcun controllo di confinamento prima di fs.writeFile.
OpenLore (clay-good/OpenLore), pacchetto npm openlore, pipeline di generazione delle spec (generatore + writer in formato OpenSpec di openlore generate). Non è il percorso deterministico analyze/orient/guardrail MCP — questo è l'unico percorso di codice nel progetto che inserisce intenzionalmente un LLM nel ciclo di generazione, secondo il README/spec del progetto stesso ("un LLM viene utilizzato solo dove la generazione è inevitabile (authoring delle spec), mai nel percorso di retrieval o guardrail").
Commit Git 1f2de00dfe75530efb256320c374dce36607ee70 (2026-07-31), versione package.json 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 — il trigger è un'invocazione locale della CLI (openlore generate) contro una directory su disco, non un servizio di rete.AC:H — lo sfruttamento richiede che l'operatore esegua il comando generate basato su LLM (non il percorso guardrail predefinito) con un provider configurato, e richiede che l'iniezione di prompt indiretta del repository analizzato faccia effettivamente emettere al modello una stringa di traversal nel campo domain — una condizione che l'attaccante influenza ma non controlla pienamente.UI:R — l'operatore deve scegliere di eseguire la generazione delle spec contro il repository malevolo.I:H / C:N / A:N — il bug è una primitiva di scrittura con contenuto parzialmente templato dall'attaccante che atterra in un percorso scelto dall'attaccante al di fuori del progetto; non è stato dimostrato che legga dati riservati o che degradi in modo affidabile la disponibilità, quindi tali metriche sono conservativamente N.ExtractedService.domain (src/types/pipeline.ts:105) viene popolato direttamente dalla chiamata LLM della Stage 3, a cui viene fornito contenuto grezzo dei file del repository in analisi e viene chiesto di restituire un array JSON di servizi:
src/core/generator/stages/stage3-services.ts:45-52 costruisce il prompt da fileChunks[i] (testo sorgente letterale del repo analizzato) e chiama pipeline.llm.completeJSON<ExtractedService[]>(..., STAGE3_SERVICE_SCHEMA).src/core/generator/schemas.ts:100 — STAGE3_SERVICE_SCHEMA.items.properties.domain è { type: 'string' }: nessun pattern, nessun enum, nessuna allowlist. Qualsiasi stringa restituita dal modello viene accettata.src/core/generator/openspec-format-generator.ts:161 — const domainName = service.domain || this.inferDomain(...) usa quella stringa così com'è.src/core/generator/openspec-format-generator.ts:563 — path: \openspec/specs/${domain.name.toLowerCase()}/spec.md`la interpola direttamente nel percorso di output dichiarato della spec. Si noti chenormalizeDomainName() (src/core/generator/openspec-compat.ts:655) esiste nel grafo delle dipendenze dello stesso file e VIENE applicata altrove (src/core/generator/openspec-writer.ts:167`, per il confronto delle directory di dominio obsolete) — ma non viene mai chiamata qui, nell'unico punto in cui il valore diventa effettivamente un percorso del filesystem.src/core/generator/openspec-writer.ts:248 — const fullPath = join(this.rootPath, spec.path); usa il semplice node:path.join, che risolve lessicalmente i segmenti ../, con nessuna chiamata al safeJoin() del progetto stesso (src/utils/path-confinement.ts) attraverso cui ogni altra superficie di percorso non attendibile in questo codebase (gli handler MCP, /api/skeleton e /api/spec-requirements di openlore view) è tenuta a passare secondo openspec/specs/mcp-security/spec.md.src/core/generator/openspec-writer.ts:277 / :294 eseguono poi writeFile(fullPath, spec.content, 'utf-8'), e ensureDir() (openspec-writer.ts:497-499) esegue mkdir(dirname(filePath), { recursive: true }) sullo stesso percorso non confinato, creando qualsiasi directory intermedia mancante lungo l'uscita dalla radice del progetto.Effetto netto: un valore domain come ../../../../../../tmp/x sopravvive intatto dalla risposta JSON dell'LLM fino a una vera chiamata writeFile, atterrando completamente al di fuori del progetto analizzato. Questo è esattamente il modello di minaccia "il contenuto del repo reindirizza una scrittura al di fuori della radice del progetto" che il file openspec/specs/mcp-security/spec.md del progetto stesso definisce esplicitamente e rafforza per le superfici MCP/serve/view (safeJoin, confinamento consapevole dei symlink, gate di copertura dei parametri di percorso) — la superficie del generatore/writer semplicemente non ha un gate equivalente.
Non è stata effettuata alcuna chiamata LLM dal vivo (la conformità del modello a un'istruzione iniettata è probabilistica e non è la parte interessante di questo bug); invece il PoC chiama il vero OpenSpecFormatGenerator e OpenSpecWriter non modificati di OpenLore con un PipelineResult il cui unico campo ExtractedService.domain contiene esattamente la forma di stringa che STAGE3_SERVICE_SCHEMA consente oggi a un LLM di restituire, e esattamente ciò che un commento in un file sorgente che istruisce "imposta domain a ../../../.../tmp/x" potrebbe elicitare dalla Stage 3 (che passa il contenuto grezzo dei file al modello).
Script: ~/engagements/openlore/source/poc-domain-traversal.ts (radice del repo del clone testato), eseguito contro un progetto scratch in ~/engagements/openlore/evidence/victim-project:
cd ~/engagements/openlore/source
npx tsx poc-domain-traversal.ts
Risultato: OpenSpecFormatGenerator.generateSpecs() (non modificato) emette una spec di dominio con
path: "openspec/specs/../../../../../../../../../../../../../../../../../../../../tmp/openlore_traversal_proof/spec.md",
e OpenSpecWriter.writeSpecs() (non modificato) la scrive — atterrando in /tmp/openlore_traversal_proof/spec.md, completamente al di fuori della radice di victim-project, mentre victim-project/openspec/specs/ contiene sempre e solo le due directory legittime overview/architecture.
Screenshot (catture autentiche Xvfb+fluxbox+xterm, scrot -w <window id>):
../evidence/poc-generator-write.png — l'esecuzione del PoC: il spec.path grezzo del generatore contenente il traversal, la riga di log del writer Wrote openspec/specs/../../.../tmp/openlore_traversal_proof/spec.md, e la conferma dello script stesso che /tmp/openlore_traversal_proof/spec.md esiste con contenuto influenzato dall'attaccante.../evidence/poc-outside-fs-proof.png — conferma indipendente: ls di victim-project/openspec/specs/ mostra solo architecture/overview (nessuna traccia del dominio malevolo all'interno del progetto), mentre ls -la /tmp/openlore_traversal_proof/ mostra il spec.md evaso che si trova direttamente sotto /tmp.Un attaccante che riesce a far eseguire a un target openlore generate (generazione di spec basata su LLM) contro un repository da lui creato — uno scenario plausibile per uno strumento CLI il cui scopo esplicito è essere puntato verso codebase di terze parti/non attendibili — può scrivere un file con contenuto influenzato dall'attaccante in qualsiasi percorso in cui l'utente OS dell'operatore possa scrivere, limitato solo dal numero di segmenti ../ necessari per evadere e dai permessi del filesystem. Non è confermato che la scrittura sia direttamente eseguibile, ma un percorso di progetto sufficientemente superficiale potrebbe consentire al traversal di raggiungere posizioni eseguite o caricate automaticamente da altri strumenti sullo stesso account (file rc della shell, directory utente di cron, configurazioni di editor/IDE), cosa che non è stata testata e non viene qui affermata.
normalizeDomainName() esistente (o una regex allowlist equivalente, ad es. ^[a-z][a-z0-9-]{0,63}$) a domain.name / service.domain nel punto in cui vengono accettati per la prima volta dalla risposta dell'LLM (groupByDomain() in openspec-format-generator.ts), non solo nel successivo sito di confronto delle directory obsolete.fullPath di OpenSpecWriter.writeSpec() attraverso il safeJoin() del progetto stesso (src/utils/path-confinement.ts) invece del semplice node:path.join, in linea con il confinamento già richiesto a ogni altra superficie di percorso non attendibile in questo codebase.pattern a STAGE3_SERVICE_SCHEMA.domain (e a qualsiasi altro campo dello schema successivamente usato per costruire un percorso del filesystem) in modo che i provider che applicano schemi di output strutturato rifiutino direttamente i valori non conformi.GeneratedSpec.path contenente .. o che si risolve al di fuori di rootPath venga rifiutato prima che OpenSpecWriter tocchi il filesystem.Dostxodjayev Abdullox
La segnalazione privata di vulnerabilità GitHub è abilitata per questo repository: https://github.com/clay-good/OpenLore/security/advisories/new (flusso GHSA standard). Confermato tramite gh api repos/clay-good/OpenLore/security-advisories che restituisce [] (nessun advisory precedente) prima di questo audit.