Skip to the content.

mixelpixx/KiCAD-MCP-Server - security scan

Repository: mixelpixx/KiCAD-MCP-Server Commit scanned: 2f05a725f9ac Scan date: 2026-07-22 Disclosure status: public (clean scan — no maintainer contact required)

Summary

Severity Count
Critical 0
High 9
Medium 20
Low 0
Info 0

Total findings: 29 (0 real after curation — 20th clean scan)

The shape of the target

KiCAD MCP Server (1.6k★, MIT) is a Model Context Protocol server that lets an AI assistant drive KiCAD for PCB design — place components, create symbols and footprints, route, run DRC/ERC, export production files. It’s a two-language bridge: a TypeScript MCP server (src/) that speaks the protocol and spawns a long-lived Python backend (python/kicad_interface.py) which talks to KiCAD through the bundled pcbnew SWIG bindings. The AI’s tool calls become JSON commands piped over stdin to that Python process.

For an MCP server the curation question is narrow and specific: can a tool argument reach a dangerous sink — a shell, a network egress it shouldn’t make, a file write outside its lane? The tool surface here is unusually wide (schematic editing, footprint generation, an autorouter shell-out, an LCSC datasheet helper, a JLCPCB parts-database downloader, Eagle/SVG import), so there were plenty of places to look. Every one of them comes back clean or by-design.

Top findings — and why each is not real

1. Six use-defused-xml-parse “highs” — the one genuine residual, and it’s DoS-only

The import features parse untrusted XML with the standard library: python/commands/eagle.py:578 (Eagle .sch), python/commands/svg_import.py:543 (SVG logos), python/commands/schematic_handlers.py:2843/3068, and python/kicad_interface.py:4142 (kicad-cli netlist) all call xml.etree.ElementTree.parse() on a caller-supplied file, with no defusedxml in either requirements.txt.

This is the only finding that describes a real class — you are parsing files a user may have received from someone else — but the impact is bounded and worth stating precisely. CPython’s ElementTree (expat) does not resolve external general entities; a SYSTEM "file:///etc/passwd" or http://… entity is not fetched, it raises. So the classic XXE outcomes — arbitrary file read and SSRF — are off the table here. What remains is entity-expansion DoS (billion-laughs / quadratic blowup): a maliciously nested design file could exhaust memory and hang the local process. That’s real, but it’s self-inflicted and local — the user asked to import the file, and the blast radius is their own KiCAD MCP process. A one-line-per-file hardening (import defusedxml.ElementTree as ET) closes it; it does not clear the “exploitability-shaped, high-confidence” bar that gates a courtesy issue.

2. Four JLCPCB SQL “highs” — the identifier-interpolation false positive

python/commands/jlcpcb_downloader.py:166 and :237 trip both sqlalchemy-execute-raw-query and formatted-sql-query:

count = src.execute(f"SELECT COUNT(*) FROM [{name}]").fetchone()[0]
...
for row in src.execute(f"SELECT * FROM [{relation}]"):

This is the single most recurring false positive in the series: the f-string interpolates an identifier, never a value. name/relation come from the source SQLite’s own schema (_relation_names()sqlite_master), are bracket-quoted, and every data column on the write side is a bound ? parameter (INSERT … VALUES (?, ?, ?, …)). No user value reaches the query text.

3. Two detect-child-process “highs” — operator-gated, and the real path is argv

src/server.ts:365 and :420 build a shell string:

exec(`"${pythonExe}" --version`, );
const testCommand = `"${pythonExe}" -c "import pcbnew; print('OK')"`;

pythonExe comes from findPythonExecutable() — a project venv/.venv, then the KICAD_PYTHON environment variable, then the KiCAD-bundled interpreter. It is operator configuration, not an MCP-client input; an operator who sets KICAD_PYTHON to a malicious string is injecting into their own machine. And the path that actually runs the backend (src/server.ts:516) already uses spawn(pythonExe, [scriptPath], …) with an argv array — no shell. Swapping the two validation exec()s to execFile is a clean defense-in-depth nit, not a remote-reachable bug.

4. Four path-join-resolve-traversal medium — constant segments

src/server.ts:95–117 flags join(binDir, "Lib", "site-packages"), join(projectRoot, "venv", …) and friends. The variable roots are __dirname-derived or platform-detected; the appended segments are string constants. There is no user-controlled path component to traverse with.

What the scanner can’t see — the sweep

Static rules under-count MCP servers (the zotero-mcp lesson), so the network/subprocess/egress surfaces got a hand sweep. All three held:

Patterns observed

The interesting thing about this repo is how little attack surface a capable MCP server can have when it commits to the local, STDIO-only transport. Every dangerous-looking primitive here — a child_process spawn, an autorouter subprocess, an XML importer, a SQL loader — is real, but each is either driven by argv (freerouting, the backend spawn), gated behind operator config (KICAD_PYTHON), or parsing files on the user’s own disk at the user’s own request. There’s no remote principal in the threat model, which collapses the interesting classes (RCE, SSRF, arbitrary write) before they start.

The freerouting handler is the highlight: it’s exactly the pattern that usually is the finding — take a filename from an untrusted-ish caller, shell out to a JAR — done right, as an argv list, with a Docker-isolated variant beside it. The datasheet helper is a nice inversion too: the “URL fetch” a scanner would want to flag is a string built from a part number and never dereferenced.

The genuine residual — stdlib ElementTree on the import paths — is worth a maintainer’s five minutes precisely because the impact is smaller than a scanner implies. It is not XXE-as-file-read (CPython won’t fetch external entities); it is entity-expansion DoS, and naming that distinction is the useful part of the write-up. The defusedxml swap is the right hardening; the “arbitrary file disclosure” reading is not.

Housekeeping-tier residuals round it out: twelve github-actions-mutable-action-tag mediums in .github/workflows/ci.yml (pin third-party actions to a commit SHA, not a mutable @v4) and two curl … | bash lines in scripts/install-linux.sh (a common installer convenience, operator-run). Both are supply-chain hygiene, not vulnerabilities.

Secrets, dependencies, and containers were fully clean: Gitleaks [], Trivy [], pip-audit 0 vulnerable dependencies.

Notes on the tool

Disclosure timeline

Reproduce

git clone https://github.com/mixelpixx/KiCAD-MCP-Server /tmp/scan-target
python scanner/run_scan.py --repo /tmp/scan-target --reports-dir ./reports/mixelpixx-kicad-mcp-server --min-severity medium