Build1 distinct publisher3 min readPublished
The author of mcpscan checked rule MCP007 against the year's four MCP path-traversal CVEs, found its sink regex only matched read calls, and closed the gap by adding write sinks that escalate to HIGH on sight.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
Read the layers before the fix. MCP007 runs three regexes per line: a SINK test for a file call, an INTERP test for a path argument built dynamically, and a TRAVERSAL test for a literal `../` that lifts MEDIUM to HIGH [8]. No AST, on purpose, so one rule file scans fast [8].
The read sink pattern included `open(` [9]. Python's `open()` is also the write sink, so mode-blind matching let some writes through by accident, while `write_text`, `write_bytes`, `fs.writeFile`, `shutil.copy` and `os.rename` matched nothing at all [10]. The author identifies that second group as the gap the mcp-atlassian shape walked through [10].
The new patterns close it unevenly. On the read side, `JS_OPEN` matches `fs.readFile` and also a bare `readFile(` with no object prefix [11]. `JS_WRITE` has no such fallback: every alternative is anchored to a literal `fs.` [12]. So `import { writeFile } from "node:fs/promises"` followed by `await writeFile(dest, body)` still produces nothing, and async `fs.rename` is absent while `renameSync` is present [13].
Then the same-line requirement. The posted `check()` does `if hit is None or not INTERP.search(line): continue` [15]. Sink and dynamic construction have to sit on one physical line, so a destination assembled on line 40 and written on line 41 reads to MCP007 as a sink taking a bare variable [16]. That is what the no-AST choice costs, and the patch does not touch it.
The severity call is the part worth arguing at review. A write-sink hit goes straight to HIGH with no literal `../` required [17], on the reasoning that an attacker-controlled destination is a worse primitive than an attacker-controlled source, because the payload usually rides in the same request, as it does in `confluence_download_attachment` [18]. The reasoning about the primitive is sound. INTERP, though, fires on `os.path.join` with a variable [19], which means `open(os.path.join(cache_dir, name), "w")` is now a HIGH finding [20]. Sink family is doing the job that evidence of traversal used to do.
One more line is load-bearing: `is_py = f.ext == ".py"`, with PY sinks for Python files and JS sinks for everything else [21]. A Go or Ruby MCP server therefore gets JavaScript patterns applied to its filesystem calls [22]. The fix did not introduce that, but it does bound what "runs across any language mcpscan supports" [24] actually buys.
For the four-out-of-four result [3] to say something about your repo, three things have to hold: the server writes files from Python or Node, the destination path is composed at the call site, and your review treated a clean MCP007 pass as coverage of the write path. The post names one of the four CVEs, CVE-2026-27825 in mcp-atlassian, which the author reports needs no auth and no restart to reach a shell [6]; the other three tools and their scores are not identified in the material [23]. The shared root cause the author describes, a caller-controlled path written with no directory-boundary check across four maintainers [5], and the claim that the write side is where the CVSS 9-plus scores landed [7], are his characterisation of the set rather than something a reader can check line by line.
Ranked by verification strength, evidence, and original report placement.
The MCP007 docstring reads 'path traversal in file-reading tools', scoping the rule to reads from day one, while the author states every real-world exploit this year happened on the write side.
The author writes that his scanner was structurally blind to the half of the bug class that is actually landing CVSS 9+ scores.
The author shipped mcpscan, a static analyzer that scans MCP (Model Context Protocol) servers for command injection, SSRF and path traversal.
Rule MCP007 was intended to be mcpscan's path traversal catch-all.
mcpscan rules are line-scan regex matching without an AST, with three layers per rule: SINK (file-open/read call), INTERP (path argument built dynamically via f-string, +, .format, or os.path.join with a variable), and TRAVERSAL (a literal ../ token on the line, which sets HIGH instead of MEDIUM).
The original PY_OPEN sink regex matched open(, .read_text(, .read_bytes(, send_file( and FileResponse(.
Distinct publishers with included, body-backed reporting in this cluster.
1 article · September 5, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
security
Reco puts 80% of employee AI tools outside IT oversight against 21% of SaaS2 distinct publishers
build
The agent asks, the gateway decides: why read-only is not a security boundary1 distinct publisher
build
The Postgres MCP server in tens of thousands of installs stopped shipping in December 20241 distinct publisher
security
Pillar chains a 9.1 SSRF to self-minted session IDs in Grafana's MCP server1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Verbatim code, unnamed CVEs
The regexes, the patched check() and the fixture output are reproduced in full, so every mechanical claim can be checked line by line without trusting the writer. The half that supplies the urgency cannot be checked at all: the four 2026 path-traversal CVEs come from the author's private research notes, one is numbered, and the CVSS 9+ framing has no score record behind it.
One repository, one operator
The only place this fix is known to run is the author's own rule file, validated against a fixture he wrote for the occasion. No download counts, dependent projects or third-party scan results appear, and the post never says whether the write sinks reached a released build of mcpscan.
Headline outruns the roll-call
"2026's worst MCP RCE" and "the one-rule fix" promise more than the text delivers: three of the four CVEs are never identified, and the new sinks leave gaps the post either lists or implies, including the same-line INTERP requirement and the missing bare writeFile( branch. Candour about the caveats keeps the overstatement modest; the missing CVE detail is what widens it.
Maintainer auditing his own tool
This is a maintainer publicising his scanner by finding it wanting, which earns trust and also keeps mcpscan in front of readers who might install it. The confessional frame does not remove the promotional interest, and no outside reviewer, coordinated disclosure process or vendor response appears in the account.
Checkable code, single voice
Only one account of this exists, written by the person being audited. What keeps the assessment above guesswork is the source code: the regexes and the check() body stand or fall on their own reading. Confidence drops wherever the story leans on the CVE tally, which nobody else in the record has confirmed.