[high] fix(radare2): run r2 command batches under radare2's sandbox - #110
Draft
elhoim wants to merge 1 commit into
Draft
[high] fix(radare2): run r2 command batches under radare2's sandbox#110elhoim wants to merge 1 commit into
elhoim wants to merge 1 commit into
Conversation
run_radare2 passes `commands` straight to `r2 -c`. The argv list is
shell-safe, but `commands` is an r2 *script*, and r2's own command language
can leave r2: `!cmd` shells out, `#!pipe sh -c cmd` does the same by another
route, `oo+` reopens the target read-write so a batch can patch the evidence
it was asked to examine, and `o /path` opens any other file the server can
read.
Verified against radare2 6.0.7. `r2 -q -c 'iI;!touch MARKER' /bin/true` --
exactly the argv main builds -- creates MARKER. With the sandbox enabled it
does not.
Enable the sandbox as the first command of every batch. It has to be a
command rather than an `-e` flag: `r2 -e cfg.sandbox=true` is applied before
the target is opened and then refuses to open it ("Cannot open ..."), so the
tool would return nothing at all. Set as the first command it takes effect
once the file is open, and r2 refuses to turn it back off ("Cannot disable
sandbox"), so appending it to an attacker-chosen command string is not
something the rest of that string can undo.
The sandbox costs only the debugger, which mulder never uses -- it does
static triage. The default batch `iI;iS;iz;afl` produces byte-identical
stdout with and without it (7531 bytes on /bin/true).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BLUF
run_radare2hands an arbitrary command batch tor2 -cwith nothing disabled. The argv list is shell-safe, butcommandsis an r2 script, not an argument list — and r2's own command language can leave r2.r2 -q -c 'iI;!touch MARKER' /bin/true— exactly the argvmainbuilds today — creates MARKER. The escape is real, not theoretical.#!pipe sh -c cmd(a second shell route),oo+thenw(reopens the target read-write and patches the evidence, though mulder never passes-w), ando /path(opens any other file the server can read).src/mulder/server/tools/extract/misc.py,run_radare2only — one constant, one 2-line helper, one call site changed. No shared abstraction, no other tool touched.The bug
commandsis caller-supplied (default"iI;iS;iz;afl"). Nothing constrains what it may contain.The fix
Why a command and not
-e cfg.sandbox=true— this is the part worth reviewing, and it is load-bearing rather than stylistic:The flag form is applied before the target is opened, and r2 then refuses to open it — the tool would return nothing at all. Set as the first command, it takes effect once the file is already open.
Why appending it to a hostile string is still safe — r2 will not let the rest of the batch undo it:
What the sandbox costs
Only the debugger, which mulder never uses — it does static triage. The default batch is byte-identical with and without the sandbox:
The sandbox does emit
INFO: Debugger commands disabled in sandbox modeon stderr;run_radare2indexesproc.stdout, so nothing reaching the case DB changes.Deliberately out of scope
run_subprocess'sOSErrorpath is labellederror_type="timeout", which is inaccurate. Noticed while reading, unrelated to this concern, left alone.fix/run-cli-tool-exit-code). That PR changesrun_cli_tool, whichrun_strings/run_hashdeep/run_exiftool/run_ssdeep/run_pascoroute through.run_radare2callssubprocess.rundirectly and does not userun_cli_tool— verified by reading the module. No other open PR touchesmisc.py(checked all 15).Verification
birth: git.6.0.7 2025-11-27), not inferred from docs.r2and areskipif-guarded onshutil.which("r2"), so they exercise the claim wherever r2 exists and skip cleanly in CI where it does not. One of them asserts the unsandboxed escape actually happens first, so the sandbox assertion can never pass vacuously; another asserts the binary really is radare2.uvx pre-commit run --all-files(new test staged first) → ruff, ruff-format, mypy all pass.uv run --locked --extra dev pytest tests/ -q→ 864 passed, nothing deselected, nothing skipped for environmental reasons.misc.pyrestored toorigin/mainand the tests kept, they fail on their assertions, not on a missing symbol:The five that pass on both trees are the live-r2 evidence tests — they characterise r2's behaviour rather than mulder's, which is exactly why they belong on both sides.
A narrowness note
An earlier draft of this test suite asserted
"-e" not in argvoutright. That is too broad: r2 itself recommends-eoptions — on an ordinary binary it printsRelocs has not been applied. Please use `-e bin.relocs.apply=true`— and a guard like that would forbid following its advice. The assertion here is narrow instead: the sandbox is set first, and no argv element outside the batch carriescfg.sandbox, so a caller cannot displace it.test_legitimate_r2_options_are_not_forbiddenpins that.Context
Recovered from closed PR #87. The rejected outcome framework and
classify_tool_exitare not reintroduced — this is a single-function change to one module. Branched fresh from currentmain(2e5432c); the closed branch was not revised in place.🤖 Generated with Claude Code