Remote Code Execution: When User Input Becomes Server Code
Remote code execution is the worst outcome a web bug can produce. Here is how attacker-controlled input reaches an execution sink, the common vectors that get there, and how to close them.
__import__('os').popen('id')At a glance
CWE-94- Interpreter
- Application runtime or operating-system execution sink
- Required condition
- Attacker-controlled data reaches an API that evaluates code or starts a process.
- Potential impact
- The application process's files, credentials, network access, and data become reachable.
- Primary defense
- Replace general execution with narrowly scoped parsers and non-executing APIs.
Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.
On this page
Remote code execution (RCE) is the outcome every other web vulnerability aspires to. Instead of reading a database or running script in someone's browser, RCE lets an attacker run arbitrary operating-system commands or program code directly on the server. It is the highest-impact class of web flaw: a single injectable parameter can hand over a foothold on the host, a launch point for lateral movement into the internal network, and ultimately full compromise of your data and infrastructure.
RCE is best understood as a category of outcome rather than a single bug. Many different roads lead there, but they share one shape: attacker-controlled input reaches an execution sink — a place where the platform turns data into running code. This post maps those roads, then walks one of them end to end.
The mental model: RCE is an outcome, not one parser
“Remote” means an attacker can influence execution across a network boundary; it does not require a particular protocol or that the process runs as an administrator. “Code execution” may start inside the application language, a template engine, a deserializer, a shell or a vulnerable native library.
| Route | Execution boundary | Typical prevention |
|---|---|---|
| Eval injection | Language runtime | Parse data or a narrow allow-listed grammar |
| Command injection | Operating-system shell | Use a library or direct process arguments |
| SSTI | Template engine | Keep template source trusted |
| Unsafe deserialization | Object reconstruction hooks | Use schema-bound data formats |
| Vulnerable dependency | Library or native process | Patch, remove or isolate the affected component |
The routes overlap in impact but not in remediation. “Block shell characters” cannot fix a Python eval, and parameterized SQL cannot fix an unsafe template compiler. First identify the execution boundary; then remove or narrow that specific interpreter.
How it happens
The root cause is always the same: input that should be treated as inert data is handed to an API that executes it. The sink varies by language and feature, but the common vectors are:
- OS command sinks — input concatenated into a string passed to a system shell (
system(),exec(),Process.Startwith a shell, backticks). eval-style constructs —eval(),Function(), dynamicimport, or template engines that compile attacker input as code.- Unsafe deserialization — turning attacker-supplied bytes back into live objects, where the act of reconstruction triggers code paths the attacker chose.
- Vulnerable libraries — a dependency with a known RCE gadget that your input happens to reach.
Here is a textbook example. A reporting feature lets users pass a small expression to customize a label, and the developer reaches for the quickest tool to evaluate it:
import flask app = flask.Flask(__name__) @app.route("/report") def report(): expr = flask.request.args.get("label") value = eval(expr) return f"<p>Computed: {value}</p>"
The intent is that label is something like 2 * unit_price. But nothing forces it to be. eval does not distinguish a harmless arithmetic string from a call into the operating system. The input is data; the code treats it as logic.
A concrete attack
The attacker does not send arithmetic. They send a payload that reaches outside the interpreter and into the host:
GET /report?label=__import__('os').popen('id').read() HTTP/1.1
Host: app.example
The server evaluates the string, which imports the os module and runs the shell command id. The response comes back carrying the proof:
<p>Computed: uid=33(www-data) gid=33(www-data) groups=33(www-data)</p>
That uid= line is the operating system answering. The attacker now knows they have code execution as the www-data user. The next request swaps id for something that opens a reverse shell, downloads a second-stage payload, or reads credentials from disk.
RCE is not really about any one function. It is about a boundary that was supposed to separate what the user said from what the server does — and a sink that erased it.
Establishing impact safely
An authorized assessment does not need to open a shell, download a file or read production secrets to prove RCE. A benign identity command in a disposable lab, a deterministic arithmetic result from an evaluator, or another uniquely attributable harmless marker is enough. Stop once the execution path and process identity are established.
Impact then comes from the process boundary: which files it can read or write, which credentials it holds, which networks it can reach and which operating-system controls contain it. This evidence-based approach avoids assuming that every execution sink immediately equals root access while still treating the finding with appropriate urgency.
What an attacker gains
Once code runs in the application's context, the attacker inherits everything that process can do:
- Full control of the host under the privileges of the web process — read, write and delete any file it can touch.
- Credential theft from environment variables, config files, cloud metadata endpoints and database connection strings.
- Lateral movement into the internal network, using the server as a pivot past the perimeter firewall.
- Persistence through web shells, cron jobs or scheduled tasks that survive a restart.
- Data destruction or ransomware across everything the service can reach.
RCE often receives a critical CVSS score, but the exact score depends on required privileges, user interaction, scope and impact. A low-privilege sandbox can reduce consequences; it does not make unintended code execution acceptable.
The fix: never turn input into code
The reliable defence is to keep untrusted input out of execution sinks entirely. The fix depends on the sink, but the principle is constant: data must never be parsed as code.
For the example above, evaluating user math does not require a general-purpose interpreter. Use a parser that understands only the operations you intend:
import ast, operator OPS = {ast.Add: operator.add, ast.Mult: operator.mul, ast.Sub: operator.sub} def safe_eval(node): if isinstance(node, ast.Constant): return node.value if isinstance(node, ast.BinOp): return OPS[type(node.op)](safe_eval(node.left), safe_eval(node.right)) raise ValueError("unsupported expression") value = safe_eval(ast.parse(expr, mode="eval").body)
Now __import__('os') is rejected before anything runs — there is no import node in the allowed grammar. Apply the same thinking to every sink:
- For OS commands, avoid the shell and pass arguments as an explicit array; allowlist permitted values.
- For deserialization, never deserialize untrusted data into arbitrary types — use a flat data format like JSON with strict schemas.
- For templates, never compile user input as a template; pass it as a value into a pre-compiled template.
- Keep dependencies patched, and run the service with least privilege so a foothold yields as little as possible.
Fixes that do not hold
- A denylist of dangerous function names: aliases, reflection and alternate runtime features make the list incomplete.
- A general-purpose language sandbox: sandboxes can be useful isolation, but they require a maintained security boundary and cannot make accidental
evalsafe. - Suppressing output: blind execution can still be observed through timing, errors or authorized callbacks.
- A WAF signature: the vulnerable execution path remains and may accept encodings or syntax the rule does not cover.
- Running as non-root: least privilege reduces impact but still exposes everything granted to the service account.
A developer review checklist
- Inventory language evaluators, process launchers, template compilers and deserializers.
- Trace request, file, message and stored values into those sinks.
- Replace general execution with a fixed operation or narrow parser.
- Remove unnecessary runtime modules, binaries and network reach.
- Apply process, filesystem, container and cloud-identity least privilege.
- Patch execution-capable dependencies and test the real deployed versions.
References
How SelfSec proves this one
Scanner behaviour- Module
- Remote Code Execution
- How it probes
- Probes interpreter, template and deserialization entry points that the crawl reached, specialised to the runtime it fingerprinted.
- How it confirms
- An out-of-band interaction originating from the target's own infrastructure ties the callback back to the exact injection point.
Reported as CWE-94 · ATT&CK T1190 · SARIF 2.1.0
A reflected response alone is not enough to promote a Remote Code Execution finding — the classical engine has to confirm the behaviour before it reaches the report. See the detection engine
SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.
Do both things about Remote Code Execution
SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.