Tracing Dangerous Python Code Paths With rce-path
I built rce-path, a Python package for tracing potentially untrusted inputs to code and shell execution, preserving static evidence, and adding optional AI review.
Finding an eval() call in a Python repository is easy. Understanding where its argument comes from takes more work.
The same problem appears with shell commands. A scanner can point to subprocess.run(), but a reviewer still needs to trace the command through assignments, string formatting, and branches. Then comes the harder question: can someone outside the intended trust boundary control that input?
I built rce-path to make that investigation more structured. It scans a local Python checkout, identifies modeled execution sinks, and records the input flow it can establish from the source. Each finding includes locations, ordered evidence, and the assumptions that still need review.
The package also supports optional OpenAI and Ollama annotations. Static scanning works independently, so you can inspect the evidence before deciding whether to involve a model.
The Problem I Wanted to Address
A suspicious function call is a starting point for security review. It leaves several questions unanswered:
- Where did the argument originate?
- Which assignments or expressions carried it to the execution call?
- Does the path depend on a branch or an unknown value?
- Is the source request data, a function parameter, or local input?
- Which parts of the flow remain unresolved?
These details matter because a function parameter is not necessarily remotely controlled. A request-derived value may pass through authentication or validation outside the scanner's view. A shell invocation using fixed literals needs a different investigation from one assembled from request data.
My goal with rce-path is to give the reviewer a traceable path and make the remaining uncertainty visible.
The Core Idea
The current release, version 0.1.0, uses Python's abstract syntax tree and per-scope control-flow analysis to track modeled inputs toward execution sinks.
A source is an input origin the analyzer recognizes. A sink is an operation that can execute Python code or a shell command. Between them, the analyzer follows supported assignments and expressions, including concatenation and f-strings. It also handles branches, early returns, and bounded loop analysis.
The report preserves the steps connecting these operations. If a call's return value cannot be resolved, the scanner records that uncertainty rather than filling the gap with an assumed data flow.
Targets are read as source data. A normal scan does not import the target package, install its dependencies, invoke its build hooks, or execute its code.
What the First Release Covers
The initial sink rules cover:
eval()andexec(), including supported built-in aliases.os.system()andos.popen().- Shell execution through
subprocess.run(),call(),check_call(),check_output(), andPopen(). subprocess.getoutput()andgetstatusoutput(), which imply shell execution.
Modeled sources include function parameters and Flask request inputs such as request.args, request.form, request.data, and request.get_data(). Environment variables, command-line input, stdin, and file reads are categorized as local inputs.
The subprocess rules account for shell behavior. An unknown shell value introduces an explicit condition. Cases using shell=False have coverage limitations; they should not be interpreted as a general safety guarantee.
This is a focused first release. It lets researchers and Python developers investigate supported execution paths without requiring an AI backend.
A Small Example
Consider this deliberately unsafe example. Save it as demo.py, but do not run it as an application:
from flask import request
import subprocess
def lookup():
host = request.args.get("host", "")
command = f"nslookup {host}"
subprocess.run(command, shell=True)
The security question is whether request data reaches a shell command. Here, the analyzer can model the request source, its assignment to host, its inclusion in the f-string, and the assignment to command before the shell invocation.
There is no route registration in this snippet. It illustrates a data-flow pattern; it does not establish that the function is deployed or reachable over a network.
To try rce-path, use Python 3.11 or newer and install the package from PyPI:
pip install rce-path
Save the example as demo.py, then generate a Markdown report:
rce-path scan demo.py --format markdown --output report.md
For this example, the report identifies the subprocess.shell rule at line 7, with static_path evidence and a network_request source. Its reachability field is network_source_modeled.
The evidence follows the request read on line 5, assignment to host, f-string construction on line 6, assignment to command, and the execution sink on line 7.
Those fields need careful interpretation. network_source_modeled means the analyzer recognized a network-input pattern. Authentication, deployment exposure, and actual exploit feasibility remain questions for human review.
Using the Python API
The command is named rce-path; the Python import is rce_path. The same scan is available through the public API:
from rce_path import ScanConfig, scan
if __name__ == "__main__":
report = scan(
"demo.py",
config=ScanConfig(ai_enabled=False),
)
print(report.summary)
for finding in report.findings:
print(
finding.rule_id,
finding.location.file,
finding.location.line,
)
for step in finding.steps:
print(
step.kind,
step.location.line,
step.description,
)
The report and findings are typed Pydantic records. You can export them with report.to_json() or report.to_markdown() and use the structured fields in your own review workflow.
Each finding carries a fingerprint, source category, evidence tier, source locations, and review state. Conditions, assumptions, and unresolved calls are available alongside the evidence. The report also records analyzed and skipped files, source content hashes, and coverage diagnostics.
Fingerprints exclude the absolute checkout root and AI annotations, which helps compare the same finding across equivalent checkouts. They are location-based, so moving code can change a fingerprint.
Where AI Review Fits
An LLM can explain a finding, identify follow-up questions, or assess whether the available evidence supports an interpretation. In rce-path, that review is an optional annotation layer.
Both provider implementations use a common evidence-and-review contract. Responses are validated, including references to evidence IDs. AI annotations do not rewrite the static finding or change its fingerprint.
For a local Ollama review, install the extra:
pip install "rce-path[ollama]"
Start your Ollama server and choose a model you already have installed:
rce-path scan demo.py --ai ollama --model YOUR_MODEL
The default endpoint is http://127.0.0.1:11434. The package does not automatically download a model or fall back to a cloud provider.
For OpenAI, install the corresponding extra and set OPENAI_API_KEY in your environment:
pip install "rce-path[openai]"
rce-path scan demo.py --ai openai --model YOUR_MODEL --allow-remote-code
YOUR_MODEL is a placeholder for an operator-selected model that supports the provider's required structured output.
OpenAI and non-loopback Ollama endpoints require explicit permission to transmit code. The package sends bounded evidence slices and applies best-effort redaction of known secret patterns. Redaction can miss sensitive content, so review what you permit it to transmit. OpenAI requests use store=False, which is not a promise of zero retention.
Review failures stay visible. A refused, unavailable, invalid, or budget-exceeded review is reported as unsuccessful rather than being presented as a completed assessment.
Running It in a Review Pipeline
For machine-readable output, scan a local checkout and save JSON:
rce-path scan ./checkout --format json --output findings.json
You can also choose a findings policy:
rce-path scan ./checkout --fail-on static-path
This fails the selected policy when the report contains a modeled static path. It does not mean the package has confirmed an exploitable vulnerability.
The CLI uses three exit codes:
- the scan completed and the selected policy passed.
- the findings policy failed.
- an operational error, incomplete scan, or failed requested AI review occurred.
The default policy is --fail-on none. Completion status takes precedence over the findings policy, so an incomplete scan cannot be treated as a successful clean scan.
A repository-level rce-path.toml can configure rules, exclusions, and analysis budgets. Repository configuration cannot enable AI or grant permission to transmit code; those decisions require runtime opt-in.
Current Limits
rce-path uses bounded static analysis. Cross-function and cross-file flow, dynamic dispatch, closures, complex heap mutation, comprehensions, and precise exception handling are outside its current coverage.
Branches are joined conservatively. A reported path can include conditions that are not jointly feasible at runtime. An unknown function is not automatically treated as a sanitizer, and a guard does not by itself prove an argument safe.
For that reason, static_path means the analyzer established a path within its models. A sink_only finding identifies a sink whose input flow remains unresolved. Neither establishes remote code execution, and the package assigns no automatic CVSS score or exploitability probability.
Read the coverage diagnostics alongside the findings. An empty completed report means "No modeled findings," not that the repository has been proved secure.
What Comes Next
The roadmap includes bounded local call summaries, additional framework and deserialization models, SARIF output, and separate human-approved validation workflows. These capabilities are planned for later phases and are not implemented in the current release.
For now, I want rce-path to make a specific part of vulnerability review easier to inspect: how an input reaches an execution operation, what evidence supports that path, and what remains unknown.
Start with a small file whose behavior you understand. Read the evidence steps, check the assumptions, and then apply the same workflow to a local repository. That is the most useful way to judge whether the package fits your research or code-review process.
Project Links
Opinions expressed by DZone contributors are their own.