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, target path lists, injection prompts, or a runnable proof of concept.
Executive summary
CVE-2026-86541 is a path-traversal vulnerability in the code.replace MCP action of knowns before 0.30.0. The handler took a caller-supplied path, used it verbatim if it was absolute and joined it to the project root otherwise, read the file, performed a literal or regular-expression substitution, and wrote the result back with os.WriteFile.
No step established that the resolved path was inside the project. An absolute path skipped the project root entirely; a relative path with parent-directory segments walked out of it.
The weakness is CWE-22: Improper Limitation of a Pathname to a Restricted Directory. The vendor scores it CVSS 4.0 7.2 High (CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:H/VA:H/SC:N/SI:N/SA:N) and CVSS 3.1 8.3 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:H). The CVE record specifically names shell startup scripts and SSH configuration files as targets.
Like CVE-2026-86439, the caller here is an AI assistant, not a browser. And like that issue, it pairs with CVE-2026-86544: code.replace was also misclassified as a read operation, so a session restricted to reading could invoke it in the first place. Read-only session, arbitrary host file write.
Fixed in knowns 0.30.0 by commit a2c98fc5c313.
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-86541 | Verified |
| Advisory | GHSA-f539-xgc6-xw7q | 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 | path argument of the code.replace MCP action |
Verified |
| Dangerous sink | os.WriteFile on an unconstrained path |
Verified |
| Accepts absolute paths | Yes, used verbatim | Verified |
| Named targets | Shell startup scripts, SSH configuration | Verified (CVE description) |
| CVSS 4.0 | 7.2 High | Verified |
| CVSS 3.1 | 8.3 High | Verified |
| Reporters | Tong Hoang Gia, Nguyen Huy Hoang | Verified |
| Fix commit | a2c98fc5c313463576c9348beeec6a74ddd7333b |
Verified |
Root-cause analysis
From internal/mcp/handlers/code.go at v0.29.1:
1 | func handleCodeReplace(ctx context.Context, getStore func() *storage.Store, getCodeRuntime func() CodeRuntime, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
Two independent escapes in four lines
1 | absPath := path |
This is a resolution helper, not a containment check, and it fails in both branches:
- Absolute path: the
ifis skipped andpathis used exactly as supplied. The project root is never consulted. This is not really traversal at all — it is an unrestricted write target handed over directly. - Relative path:
filepath.Joincleans the result lexically and resolves parent-directory segments outward, past the project root.
The pattern is deceptively reasonable. “If it is already absolute, we are done; otherwise make it absolute relative to the project” is correct resolution logic. It is only wrong because nothing afterwards asks whether the resolved path is somewhere the caller is allowed to write. Resolution and authorisation got conflated, and only resolution was implemented.
There is also no symlink handling, so even a correctly contained lexical path could point outside the root through a link inside it.
The substitution constraints are weaker than they look
At first glance the write seems bounded: the file must already exist (os.ReadFile must succeed), needle must be present, and by default more than one occurrence is rejected. That is not much of a constraint:
allow_multiple_occurrencesis caller-supplied, so the multiplicity guard is off whenever the attacker wants it off;mode: "regex"compilesneedleas a Go regular expression, and a pattern that matches broadly turns a targeted substitution into a wholesale rewrite of the file’s contents;- the file only has to exist, and the most valuable targets on a developer machine — shell startup files, SSH configuration, git configuration — generally do.
Inferred: in practice this is a full-content write against any existing file the process can write, not a surgical patch. The VI:H/VA:H rating reflects that.
The permissions layer did not compensate
In the same release, code.replace had no entry in the action registry and inherited CapRead from the tool-level fallback. That is CVE-2026-86544. The practical consequence for this issue is that the capability gate, which existed and worked, was gating the wrong verb — so a session deliberately restricted to reading could still reach os.WriteFile.
Inferred: this is the more general lesson of the pair. A path-containment bug and a classification bug are each individually recoverable; together they remove both the outer and inner control on the same call.
The neighbouring handlers shared it
The fix also rewrites lspPathRequest, which fed handleCodeRename, handleCodeReplaceBody, handleCodeInsert, and handleCodeDelete with the identical IsAbs-or-Join idiom, and changes applyWorkspaceEdit to take the project root so that LSP-driven multi-file edits are contained too. Inferred: the pattern had been copied across the file, which is the usual reason a single-line bug appears in five handlers at once.
Why user interaction is in the vector, and why it is cheap
The vector carries UI:P (CVSS 4.0 passive) / UI:R (CVSS 3.1 required). That is accurate — somebody must be running an assistant session against the knowns MCP server for the call to happen.
Inferred: in an agentic workflow, this requirement is nearly free for an attacker. The “interaction” is a developer doing their normal job: opening a repository, asking an assistant to make a change, letting it read a README or an issue thread or a dependency’s documentation. If any of that content is attacker-influenced, the tool call can be too. The user does not approve a write to ~/.ssh/config; they approve “help me refactor this module”, and the tool argument is chosen downstream by the model.
The attack path:
1 | Untrusted content the assistant reads |
Impact
Verified impact
- Files outside the project root could be overwritten.
- Both absolute paths and relative paths with traversal sequences were accepted.
- Integrity and availability impacts are High; confidentiality is Low (the read that precedes the write returns nothing to the caller directly, but the substitution result is observable).
- The CVE record names shell startup scripts and SSH configuration files as realistic targets.
Deployment-dependent consequences
The following are inferred:
- code execution at the developer’s next shell start, git operation, or tool invocation, depending on which configuration file was modified;
- redirection of SSH connections or acceptance of attacker key material, converting a local write into remote access;
- modification of source, build scripts, or CI configuration in repositories outside the current project — a supply-chain path that reaches production via the developer’s legitimate commits;
- corruption or truncation of arbitrary files through a broadly matching regular expression, which is the availability impact;
- silent, targeted edits to code the developer is about to review, where a small change in a large assistant-generated diff is easy to miss.
Inferred: that final point is specific to agentic tooling and under-appreciated. Developers reviewing AI-generated changes are already scanning a larger diff than they wrote. A one-line modification introduced by a compromised tool call is competing for attention with dozens of legitimate ones.
Safe validation and detection
Inventory and configuration review
- Identify knowns installations and versions; anything before 0.30.0 is affected.
- Identify which assistants had the knowns MCP server configured, and whether code tools were auto-approved.
- Establish the window during which an affected version was in use with an assistant attached.
- Review the integrity of files outside the project that the account could write — this is the exposure scope, not the project directory.
Telemetry and integrity review
- MCP tool invocations of
code.replace,code.rename,code.insert,code.replace_body, andcode.deletewherepathis absolute or contains parent-directory segments; - any
code.replacecall inregexmode with a broadly matchingneedle, or withallow_multiple_occurrencesset; - modification times on shell startup files, SSH configuration, git hooks, and editor configuration that cluster around assistant activity;
- for repositories under version control, unexplained working-tree changes; note that files outside a repository will not be covered by this check;
- host filesystem audit events showing the knowns process writing outside the project root.
Inferred: assistant session transcripts are again the decisive artefact, because they contain the exact path argument. Retain them.
Remediation and hardening
Required remediation
Upgrade to knowns 0.30.0 or later. The fix replaces the resolution idiom with a containment-enforcing resolver and adds a second, independent check before any mutation:
1 | absPath, err := safepath.ResolveProject(projectRoot(store), path) |
safepath.ResolveProject still accepts native absolute paths — the tool legitimately needs to work with them — but only when they resolve inside the root:
1 | // ResolveProject resolves a project path inside root. Native absolute paths |
That distinction is the well-designed part of the patch. The requirement was never “reject absolute paths”; it was “the final target must be inside the root”. Expressing it that way keeps the feature working while making the unsafe case impossible.
ensureCodeMutationPath then resolves symlinks with filepath.EvalSymlinks and re-checks containment against the real root, so a link planted inside the project cannot redirect a write outside it. The same commit applies both checks to lspPathRequest and handleCodeRename, and threads the project root into applyWorkspaceEdit(projectRoot(store), changes) so LSP-driven multi-file edits are contained as well.
Separately, the commit adds confirmed and reason arguments to the code tool for action=delete, so destructive operations require explicit acknowledgement rather than inheriting the same path as an edit.
Defense in depth
- Do not auto-approve MCP code-mutation tools; require confirmation that shows the resolved absolute path, not the argument as supplied.
- Run assistants against a filesystem boundary enforced by the OS — container, dedicated user, or sandbox — so an application-level containment bug is not the only control.
- Keep SSH keys, cloud credentials, and shell startup files outside the reach of tooling that edits code, where your workflow allows it.
- Enable file integrity monitoring on the small set of files that grant execution or access: shell rc files, SSH config, git hooks, editor startup configuration.
- Review AI-generated diffs against the set of files touched before reviewing their contents; an unexpected path is faster to spot than an unexpected line.
Timeline
| Date | Event | Source certainty |
|---|---|---|
| 2026-08-15 | Fix commit a2c98fc5c313 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-86541 reserved and published | CVE record metadata |
| 2026-09-08 | This analysis published | This site |
Lessons for builders
Resolving a path is not authorising it
if !filepath.IsAbs(p) { p = filepath.Join(root, p) } answers “where does this point?” and never asks “is the caller allowed to write there?”. Those are two operations. Write them as two, or use one function whose contract covers both.
Accepting absolute paths is fine; trusting them is not
The fix did not remove the capability. ResolveProject accepts absolute input and verifies containment. Prefer that shape over a blanket rejection — it keeps legitimate workflows working and removes the temptation to add an unchecked bypass later.
Resolve symlinks before enforcing policy
Lexical containment is necessary and insufficient. If the write matters, canonicalise with EvalSymlinks and check the real path, as ensureCodeMutationPath does.
Copied idioms copy their bugs
The same four lines appeared across several handlers, so one review miss became five vulnerable entry points. When you find a path bug, grep for the idiom rather than fixing the reported call site.
Two weak layers can fail as one
code.replace was both misclassified and uncontained. Neither the permission gate nor the path check stopped the write. When reviewing a mutating operation, verify each layer independently — a control you assume is upstream may be gating the wrong verb.
Related advisories in this set
- CVE-2026-86544 — the classification half of this pair, fixed in the same commit
- CVE-2026-86439 — path traversal through MCP doc and memory tools
- 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-86540 — arbitrary code execution via the LSP binary override
- CVE-2026-86775 — unauthenticated path traversal through the HTTP Document API
References
- CVE.org record: CVE-2026-86541
- NIST National Vulnerability Database: CVE-2026-86541
- GitHub Security Advisory: GHSA-f539-xgc6-xw7q
- VulnCheck advisory
handleCodeReplaceat v0.29.1- Fix commit
a2c98fc5c313 - 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.