pyghidra-lite v0.7.0 — Locked Down
"A setting you can change mid-session is a setting an attacker can change mid-session."
Security-hardening release · immutable-by-design config · adversarial red-team suite · real-Ghidra E2E CI
This release makes the server's security posture structural rather than incidental: the configuration is frozen once the server goes live, the run-time remote-code-execution path is gone, and every claim is backed by an adversarial test that performs the attack and asserts it fails closed.
| Area | Change |
|---|---|
| 🔒 Immutable config | ServerConfig is frozen, built once, and locked at go_live(). Changing a setting mid-session raises ConfigLockedError. |
| 🔒 No remote code exec | search(type="extract") no longer fetches+runs an npm package on every call. |
| 🔒 Transport | DNS-rebinding protection, required bearer auth off-loopback, ipaddress-based loopback detection. |
| ⚡ Performance | info/code/xrefs moved off the event loop — one decompile no longer freezes the shared HTTP server. |
| 🧪 Tests | Red-team gauntlet + holder-impenetrability checks + real-Ghidra E2E CI. |
Configuration is immutable while serving SECURITY
ServerConfig is now a frozen value object, built exactly once at startup through a single writer (configure_server) and sealed at the serve boundary by go_live(). After that, any attempt to change a security setting — restrict paths, bind host, auth token, runtime home — raises ConfigLockedError.
Why this matters (the threat model)
The defended surface is the agent-driving-the-tools path and accidental/buggy mutation: no MCP tool can widen restrict_paths, flip shared, disable auth, or add an allowed Host while a connection is live. To change a setting you stop the process and re-run the CLI. A removed hidden writer (_init_backend used to mutate runtime_home in place, inside the lifespan, after go-live) is gone — the resolved runtime home is now persisted before the lock.
This is not a sandbox against arbitrary code execution in the server process; it makes the tool/automation surface have no expressible way to change a security setting mid-session.
No run-time remote code execution SECURITY
search(type="extract") previously ran bun x bun-extract-bundled, which resolved and executed an npm package from the network on every call. It now invokes a locally pre-installed, pinned extractor directly (fixed argv, no shell, no package-manager launcher), or returns an actionable error.
Transport & input hardening SECURITY
- DNS-rebinding protection (Host/Origin validation) on all HTTP/SSE binds.
- Constant-time bearer-token guard (
--auth-token/PYGHIDRA_LITE_AUTH_TOKEN), required for non-loopback binds;--allowed-hostfor fronting. ipaddress-based loopback detection (closes::1, the::wildcard, and hostnames that slipped past the old literal== "127.0.0.1"); fails closed on ambiguous hosts.- Import-path TOCTOU defense — canonical path re-validated immediately before import; errors report
requested=andresolves_to=. - Trailing-newline id bypass fixed —
^[0-9a-f]{16}$accepted"<id>\n"(Python's$matches before a final newline); both validators now anchor with\Z. - Error redaction strips server-side absolute paths from outward-facing exceptions.
- MCP tool annotations — every tool publishes
ToolAnnotations(read-only / destructive / idempotent / open-world) for safe client auto-approve UX.
Performance
info, code, and xrefs (and the blocking bodies of functions/search) ran JVM work directly on the async event loop — a single decompile froze the whole shared HTTP server. They're now async via asyncio.to_thread.
Also
GhidraToolscached per binary handle (function-name index + list caches survive between calls).load()caches the content hash by(path, mtime, size)— no re-hashing an already-analyzed binary.xrefs(depth>1)call graphs bounded (max nodes/edges) and flaggedtruncated.- Fixed a dead
depth = min(depth, 5)line inxrefsthat would raiseUnboundLocalErrorondepth>1.
Testing
Adversarial red-team gauntlet — tests/test_red_team.py
Each test performs an attack and asserts it fails closed: restrict-path escape + TOCTOU, id injection (incl. the newline bypass), a live trojan-on-PATH trap proving extraction never runs a package launcher (real processes), DNS-rebinding rejected 421 end-to-end through the real ASGI app, the auth-bypass matrix, loopback fail-closed, job-queue cap, error redaction, worker self-spawn, and holder-impenetrability checks — config cannot be mutated while live, plus a tripwire that fails if any @mcp.tool gains a settings-mutation surface.
Real-server E2E — tests/test_security_e2e.py + CI
Gated by PYGHIDRA_E2E, with a security-e2e workflow (JDK 21 + pinned Ghidra) that boots an actual pyghidra-lite serve and runs the attack matrix over real TCP sockets: missing/wrong token → 401, forged Host → 421 (repeatable — the allowed-host set can't be widened at runtime), legit request passes both guards.
uv run pytest → 317 passed, 6 skipped (the 6 are the real-Ghidra E2E).
Upgrade
pip install --upgrade pyghidra-lite