Use GitHub's private vulnerability reporting on this repository (Security → Report a vulnerability). Please do not open a public issue for security problems. We aim to acknowledge within 3 business days.
spectra runs on your machine over stdio MCP only and opens no network listener; it attaches
only to debuggable builds on Android devices and iOS simulators that you connect.
spectra_evaluate and the jdb path execute code inside the debuggee by design, so any
local caller that can invoke the MCP server can run code in the attached application. Android
package names reach adb shell (re-parsed by the device's sh -c), JVM class/method targets
reach jdb stdin (line-oriented), and iOS bundle IDs reach xcrun simctl as an argument;
each is validated with re.fullmatch in core/validation.py before any subprocess or
debugger command is sent. Android serials and simulator UDIDs are allowlisted (serial
characters [A-Za-z0-9._:-], first character alphanumeric; UDID = UUID or booted; never a
leading -) and only ever passed as argv, never through a shell. Each jdb session runs in
a private working directory per session so .jdbrc / jdb.ini files in the working directory
are ignored (a jdb rc file in the user's home directory is still read, as jdb always does).
The local MCP usage log is written on your machine
only and is redacted by default (SPECTRA_USAGE_LOG=off|redacted|full, default redacted).
On the iOS simulator the debuggee is a macOS process running with your user's privileges, so
an expression that calls into the process (including C or Objective-C/Swift function calls
such as (int)getpid()) runs on your Mac as you. spectra sends iOS expressions to lldb-dap
in an expression-only (watch) context and refuses backtick-prefixed input, so
LLDB debugger commands (platform shell, script) cannot be issued through
spectra_evaluate; because target expressions themselves still execute inside the simulator
process on the host, treat access to the MCP server as local code execution.
An adversarial security review and first-time user ("stranger") run conducted on 2026-10-24 evaluated the pre-release v0.1.0 candidate (private linc-spectra commit 88cc41f, candidate wheel SHA-256 d7a441307df273487cf02c3c380867fc203c92fa25fbfaf306becaba5ef5dce3). Testing used an isolated session with no access to fix history, operating in-process against recording fakes for adb, xcrun, jdb, kotlin-debug-adapter, and lldb-dap, plus live verification against a booted iOS Simulator with Xcode lldb-dap.
Across 42 adversarial attempts, 29 held and 16 security/reliability findings were identified (1 High confirmed on a live iOS Simulator, 3 Medium, 12 Low across 6 groups), all fixed before release:
- LLDB debugger command execution in iOS
spectra_evaluate(High):spectra_evaluateforwarded iOS expressions tolldb-dapwithcontext="repl", allowing backtick-prefixed or command-shaped inputs (platform shell,script) to execute LLDB debugger commands on the macOS host. Fixed by sending iOS evaluations in expression-onlywatchcontext and refusing backtick-prefixed expressions before they reach the adapter. - Structured logging on MCP
stdoutinspectra bellows(Medium): Defaultstructlogconfiguration wrote log lines tostdouton the first tool call, corrupting the stdio JSON-RPC stream. Fixed by routingstructlogand standard-library logging tostderrbefore starting the MCP stdio server. - Unbounded
wait_sand worker pool exhaustion on JDB collect tools (Medium): Passing a huge or infinitewait_stospectra_exception_infoor manualspectra_method_localstied up all 8 worker threads sospectra_sessionsandspectra_detachhung, and wait loops ignored subprocess EOF or session closure. Fixed by rejecting non-finite or negativewait_s, cappingwait_sat 180 s, waking waits on session close orjdbexit, keeping waiting sessions listed and detachable, rejecting concurrent collects on the same session, and runningspectra_sessions,spectra_detach, andspectra_usage_statsoutside the blocking pool. - Quadratic JDB stdout reader and reader thread leak (Medium): Reading
jdboutput character-by-character caused multi-second stalls on long single-line local values and left the reader thread spinning after session close. Fixed by reading 64 KiB binary chunks with incremental UTF-8 replacement decoding, truncating lines above 64 KiB, bounding the line queue to 10,000 lines, and joining the reader thread onclose(). - Unbounded manual
max_stepsinspectra_method_locals(Low): Largemax_stepsvalues issued unboundednextcommands without returning. Fixed by requiringmax_stepsin0..16and enforcing a 20 s manual stepping budget. - Hostile adapter framing and queue growth (Low): Malformed DAP
Content-Lengthheaders, newline-less header orstderrstreams, unrequested responses, and synthetic event names could grow memory. Fixed by capping header lines at 1 KiB andContent-Lengthat 16 MiB, dropping responses without a pending request, capping distinct event queues at 64 (always admitting standard DAP events), readingstderrin 4 KiB chunks, and keeping only the first value per local name within a single JDB reply. - Android serial and iOS Simulator UDID validation (Low): Device identifiers and leading-dash values were not validated before reaching
adborxcrunargv. Fixed by allowlisting Android serials ([A-Za-z0-9._:-], alphanumeric first character, max 128 chars) and iOS Simulator UDIDs (canonical UUID orbooted), capping package, bundle ID, class/method, and path lengths, bounding breakpoint line numbers andframe_id, and enabling strict MCP argument types. - Non-UTF-8 JDB output handling (Low): Invalid UTF-8 bytes in debuggee strings terminated the JDB reader thread and emptied captured locals. Fixed by decoding with an incremental UTF-8 replacement decoder.
- macOS
lldb-dapdiscovery and system stub verification (Low):spectra doctorandspectra_attachmissedlldb-dapunder Xcode / Command Line Tools when not onPATHand reported/usr/bin/jdband/usr/bin/xcrunstubs as healthy when no JDK or developer directory was configured. Fixed by resolvinglldb-dapviaxcrun -f lldb-dap, verifying/usr/bin/jdbvia/usr/libexec/java_homeand/usr/bin/xcrunviaxcode-select -p, isolating eachjdbsession in a private temporary directory, and returning actionableadbmissing-tool errors. - Rotated usage log permissions and link safety (Low): Rotating a pre-existing world-readable usage log preserved its permissions on the rotated file, hard-linked log paths were accepted, and tool results returning an
"error"dictionary were recorded assuccess: true. Fixed by tightening the descriptor to0600withfchmodbefore rotation, refusing files withst_nlink > 1, recording"error"dictionaries assuccess: false, and redacting the home directory prefix to~inspectra_usage_stats.
Candidate wheel SHA-256 (pre-fix): d7a441307df273487cf02c3c380867fc203c92fa25fbfaf306becaba5ef5dce3.
A second verification run on 2026-10-25 re-executed all 42 adversarial vectors, the live iOS Simulator lldb-dap evaluation check, and the stranger run against the fixed release candidate wheel (48e96ce7b15124502f06ae387db02e0fc58f2a64c191889b43dd52445ef34bd0, built from private linc-spectra commit 92b51f3).
Results: 0 High, 0 Medium, 0 Low reproduced. All 42 vectors, the live iOS Simulator watch-context and backtick-refusal checks, and the stranger run passed cleanly.
The reviewed v0.1.0 release candidate contains all fixes above:
- Source: private
linc-spectracommit92b51f3(treeff0aa93d098ad2e9f196cafba3ccc17695309f88). Commits after it and before thev0.1.0tag change onlySECURITY.mdand its test, not the package. - Reviewed candidate wheel SHA-256:
48e96ce7b15124502f06ae387db02e0fc58f2a64c191889b43dd52445ef34bd0 - The wheel published on PyPI will have a different SHA-256 even though its contents are the same, because a wheel records each file's timestamp and the published build's timestamps differ from the candidate's.
- After
v0.1.0is published, this section names the publicv0.1.0tag, its commit, and the SHA-256 of the wheel PyPI serves. To check that hash:pip download linc-spectra==0.1.0 --no-deps -d .thenshasum -a 256 linc_spectra-0.1.0-py3-none-any.whl.