Scope and safety. This article analyses a fixed, publicly disclosed vulnerability using the vendor’s own published source references. It describes the defective data flow and the remediation. It does not include working traversal strings, injection prompts, or a runnable proof of concept.
Executive summary
CVE-2026-86439 is a path-traversal vulnerability in the MCP tool surface of knowns before 0.30.0. knowns exposes its document store and memory store to AI coding assistants over the Model Context Protocol. The path and ID arguments those tools accept were joined onto the managed storage directories without any containment check, so a caller could read, create, overwrite, and delete files outside the project directory.
The weakness is CWE-22: Improper Limitation of a Pathname to a Restricted Directory. The vendor scores it CVSS 4.0 8.7 High (CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) and CVSS 3.1 8.8 High (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H). All three impact metrics are High — this is a read and write and delete primitive.
What makes this issue distinct from the other traversal bugs in the knowns set is the caller. The other entry points are HTTP endpoints an attacker reaches directly. This one is an MCP tool argument, which means the attacker’s most natural path is not the network at all. It is content — a README, an issue, a dependency’s documentation, a web page — that a coding assistant reads and is influenced by. The model constructs the tool call; the model has legitimate access; the traversal happens on the attacker’s behalf.
Fixed in knowns 0.30.0 by commit 09c5a96fd581.
Evidence labels used below:
- Verified means stated by the CVE record, the GitHub Security Advisory, or the project’s published source and release material.
- Inferred means a deployment or attack-path consequence derived from those facts and clearly marked as such.
Key facts
| Item | Finding | Confidence |
|---|---|---|
| Identifier | CVE-2026-86439 | Verified |
| Advisory | GHSA-9gfj-28hw-jchp | Verified |
| Product | knowns (pkg:npm/knowns, Go implementation) |
Verified |
| Affected versions | All versions before 0.30.0 | Verified |
| Fixed version | 0.30.0 | Verified |
| Weakness | Path traversal, CWE-22 | Verified |
| Entry point | Filesystem path arguments in MCP tool calls | Verified |
| Dangerous sinks | DocStore.Get, MemoryStore.GetInLayer and the memory write paths |
Verified |
| Operations exposed | Read, create, overwrite, delete | Verified |
| File-type constraint | Markdown files, per the CVE description | Verified |
| CVSS 4.0 | 8.7 High | Verified |
| CVSS 3.1 | 8.8 High | Verified |
| Reporters | Tong Hoang Gia, Nguyen Huy Hoang | Verified |
| Fix commit | 09c5a96fd5817b941dc86669278c1a17db10ed4e |
Verified |
Why the MCP boundary changes the threat model
MCP tool arguments feel like internal function parameters. They are not. In an agentic system, they are the output of a language model that has just processed untrusted text.
The chain looks like this:
1 | Untrusted content the assistant reads |
Three consequences follow, and they are the reason this class deserves separate treatment from ordinary web traversal:
The confused deputy is the model. The MCP server authenticates and authorises the session, not the intent. A session with legitimate doc access is exactly what a prompt-injection payload wants to borrow. PR:L in the vector reflects that the attacker needs a session — but in agentic deployments, the session is supplied by the victim.
Argument validation is the only remaining control. There is no user in the loop reviewing a path field. Tool arguments are typically auto-approved for read-classified operations, and — as CVE-2026-86544 documents in the same codebase — operations are not always classified correctly in the first place.
Feedback closes the loop. knowns’ memory store is designed to persist across sessions. Inferred: a traversal that can write memory entries can plant content that the assistant will read in a future session, giving the attacker a durable foothold in the model’s context rather than a one-shot influence.
Root-cause analysis
DocStore.Get — the classic lexical join
From internal/storage/doc_store.go at v0.29.1:
1 | // Get retrieves a doc by its relative path (without .md extension). |
There is normalisation here — a leading slash is trimmed, an .md suffix is trimmed and re-added — and it is easy to mistake that for validation. It is not. Trimming a leading / prevents an absolute path from replacing the base in filepath.Join, but it says nothing about parent-directory segments inside the path. filepath.Join then cleans the result lexically and resolves those segments outward, past docsDir().
The forced .md suffix is a real constraint, and it is why the CVE description says “arbitrary Markdown files” rather than “arbitrary files”. Inferred: on a developer machine that constraint is weaker than it sounds. Documentation, notes, agent instruction files, and repository READMEs are all Markdown, and those are precisely the files whose content steers both humans and assistants.
The same function then loops over the imports directory and builds two more candidate paths from the same unvalidated input, so a single call had several joins to escape from.
MemoryStore.GetInLayer — a prefix that defeats itself
From internal/storage/memory_store.go at v0.29.1:
1 | // GetInLayer retrieves a memory entry by ID from a specific layer only. |
Where MemoryFileName is:
1 | func MemoryFileName(id string) string { |
This is a small, instructive failure. layer is validated against an enumeration; id is not validated at all. Two things then go wrong at once:
The memory- prefix looks like it constrains the filename, and does not. Concatenating a prefix onto an ID that may contain separators produces a string whose first path segment is memory- plus the ID’s first segment — and everything after the first separator is a fresh segment that the prefix cannot reach. If the ID begins with parent-directory syntax, filepath.Join cleans memory-.. as an ordinary directory name and then the following .. removes it again, leaving the traversal intact and the final basename entirely attacker-controlled.
This is worth stating as a general rule, because the pattern is everywhere: string concatenation does not survive path normalisation. A prefix or suffix only constrains the filename if the value between them is guaranteed to contain no separators — which is a validation requirement, not a formatting one.
The .md suffix is likewise per-segment. It attaches to the last segment of whatever path the ID describes, not to a filename inside the memory directory.
Because the memory store’s create, update, and write paths built filenames the same way, the primitive was not read-only. That is where VI:H and VA:H come from: the CVE record states that attackers can “read, create, overwrite and delete files outside the project directory”.
The common shape
Both sinks share one root cause: an untrusted string was treated as a name by the code’s mental model and as a path by the filesystem. Nothing in between forced those two views to agree.
Impact
Verified impact
- Files outside the project directory could be read, created, overwritten, and deleted.
- All three CIA impact metrics on the vulnerable system are rated High.
- The operations execute with the privileges of the knowns process — on a workstation, the developer’s own account.
- Low privileges are required (
PR:L); no user interaction is (UI:N).
Deployment-dependent consequences
The following are inferred:
- disclosure of documentation, notes, and specifications belonging to other projects on the same machine;
- overwriting agent instruction files, project READMEs, and architectural decision records — content that both humans and assistants treat as authoritative;
- planting persistent content in the global memory layer, which is read in future sessions and by other projects;
- deletion of documentation and memory entries, causing loss of work that may not be under version control;
- laundering of an indirect prompt injection into a filesystem action: the untrusted text that caused the tool call is transient, but the file it wrote is not;
- chaining with the other issues in this set — a traversal that can write Markdown into a project is a good position from which to influence a developer who is about to run CVE-2026-86541-style code edits.
Inferred: the durability point is the one defenders most often miss. Prompt injection is usually discussed as an ephemeral influence on one response. A tool that converts it into a file write makes it persistent, and the persistence lives outside the model, outside the session, and outside anything the model provider can revoke.
Safe validation and detection
Inventory and configuration review
- Identify knowns installations and versions; anything before 0.30.0 is affected.
- Identify which assistants are wired to the knowns MCP server, and whether their doc and memory tools run with auto-approval.
- Review what untrusted content those assistants routinely ingest — cloned repositories, dependency documentation, imported docs, fetched pages.
- Check the memory store for entries nobody remembers creating, particularly in the global layer.
Telemetry to review
- MCP tool invocations where a
pathoridargument is not a plain relative name; - any such argument containing parent-directory segments, leading separators, or backslashes;
- Markdown files modified outside the project tree at times matching assistant activity;
- new or altered agent instruction files and READMEs that no commit explains;
- memory entries whose IDs do not match the tool’s own naming conventions;
- host filesystem audit events showing the knowns process touching paths outside its managed roots.
Inferred: MCP session transcripts are the highest-value artefact here, because they record the argument that was actually sent. If your assistant retains them, they answer the “did this happen” question directly. If it does not, that gap is itself worth fixing before the next MCP CVE.
Remediation and hardening
Required remediation
Upgrade to knowns 0.30.0 or later. The fix introduces the internal/safepath package and a validated memory ID format.
Document paths are now resolved with containment, including in the imports loop where the original code built candidates by hand:
1 | importDocsDir := filepath.Join(ds.importsDir(), importSource, "docs") |
Memory IDs are validated against an explicit format rather than being trusted to be well behaved:
1 | var memoryIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`) |
Two details in that pattern are deliberate and worth copying:
- It is an allowlist anchored at both ends. No separators, no NUL, no traversal, no Windows device syntax — because none of those characters are in the permitted set at all, rather than because each was individually blocked.
- It constrains the first character and the last. Requiring an alphanumeric start rules out leading dots and hyphens; the explicit trailing-dot rejection handles Windows filename normalisation, which silently strips trailing dots and would otherwise let two distinct IDs map to one file.
The validator is then called on Get, GetInLayer, Create, Update, and the internal writeExisting path — every operation that turns an ID into a filename, not just the read path that the advisory highlights.
Defense in depth
- Treat MCP tool arguments as untrusted input from a language model, and validate them at the handler with the same rigour you would apply to an HTTP request body.
- Do not auto-approve MCP tools that can write or delete, even when they are classified read-only — see CVE-2026-86544 for why that classification may be wrong.
- Scope the assistant’s filesystem access to the project, ideally with an OS-level boundary (container, dedicated user, or sandbox) rather than an application-level one.
- Keep the global memory layer under review; content that persists across projects is content that persists across trust boundaries.
- Prefer storage APIs that take an opaque key and own the filename entirely, so the caller never supplies path syntax.
Timeline
| Date | Event | Source certainty |
|---|---|---|
| 2026-08-15 | Fix commit 09c5a96fd581 authored in the knowns repository |
Repository metadata |
| 2026-08-16 | Public disclosure date recorded on the CVE record | CVE record datePublic |
| 2026-09-07 | CVE-2026-86439 reserved and published | CVE record metadata |
| 2026-09-08 | This analysis published | This site |
Lessons for builders
MCP tool arguments are untrusted input
This is the headline. The parameters of an MCP handler arrive from a model that has been reading attacker-reachable text. Every validation habit built for HTTP handlers applies unchanged, and the absence of a human in the loop makes them more important, not less.
Prefixes and suffixes are not filename constraints
"memory-" + id + ".md" reads as though the filename is fixed. It is fixed only if id contains no path separators — and nothing guaranteed that. Formatting is not validation. If a value must be a single segment, assert it.
Normalisation is not validation
Trimming a leading / and an .md suffix made the input look handled. Ask of every normalisation step: what property does this establish? “Cannot escape the base directory” was never established here.
Validate every field, not the field that looks risky
layer had an enumeration check. id had nothing. The developer was clearly security-aware; the check simply followed the parameter with the more obviously constrained domain. Trace each argument to its sink on its own.
Persistent memory is a persistent attack surface
A store designed to carry context across sessions will also carry an injected instruction across sessions. Treat writes to long-lived agent memory as privileged operations, and make their contents auditable by a human.
Related advisories in this set
- CVE-2026-86543 — unauthenticated management API exposure
- CVE-2026-86538 — unauthenticated arbitrary file read via
templateFile - CVE-2026-86542 — unauthenticated arbitrary file write via import name
- CVE-2026-86541 — path traversal in the
code.replaceMCP action - CVE-2026-86544 — authorisation bypass via misclassified code actions
- CVE-2026-86540 — arbitrary code execution via the LSP binary override
- CVE-2026-86775 — the HTTP-facing variant of this same storage-layer defect
References
- CVE.org record: CVE-2026-86439
- NIST National Vulnerability Database: CVE-2026-86439
- GitHub Security Advisory: GHSA-9gfj-28hw-jchp
- VulnCheck advisory
DocStore.Getat v0.29.1MemoryStore.GetInLayerat v0.29.1- Fix commit
09c5a96fd581 - knowns v0.30.0 release notes
Reported by Tong Hoang Gia and Nguyen Huy Hoang. Recheck the primary sources above during remediation; public vulnerability records are frequently revised after publication.