Blog · E.23 · Technical

Command injection explained

Some apps hand user input to the operating-system shell to do their work - run a utility, ping an address, convert a file. If that input isn't handled carefully, an attacker can slip in their own commands and run them on your server. That's OS command injection: one of the shortest paths from a text box to full server compromise. Here's how it works, why it's so severe, the blind variants that scanners miss, and how to design it out.

Command InjectionOS CommandRCEBlind InjectionWeb AppSec
Command injection: User Input → Shell · Shell Metacharacters · Arbitrary Command Execution · Full Compromise · Blind / Out-of-band · Fix = Don't Call the Shell / Pass Args Safely Command injection: User Input → Shell · Shell Metacharacters · Arbitrary Command Execution · Full Compromise · Blind / Out-of-band · Fix = Don't Call the Shell / Pass Args Safely
// TL;DR

Command injection (OS command injection) happens when an app passes untrusted input into a system shell without proper handling, letting an attacker inject and run their own OS commands on the server. It arises from building shell commands by concatenating user input; the attacker adds shell metacharacters to append commands. Because they run with the app's privileges, the result is arbitrary code execution and full server compromise - critical severity. Blind variants (no output returned) are confirmed via time delays or out-of-band callbacks - exactly where manual testing beats scanners. The durable fix: don't call a shell with user input - use safe language APIs, or pass arguments as a separate array so no shell interprets them; filtering alone is unreliable. Detail below; part of every web app test.

// 01 How command injection happens

Applications sometimes need to run an operating-system program to get their job done - invoke an image converter, ping a host, call a command-line tool. The vulnerability appears when the app builds that shell command by concatenating user input. Say a feature pings a user-supplied address by constructing ping <user_input> and handing it to the shell. A normal user types an address; an attacker types 8.8.8.8; cat /etc/passwd - and the shell, seeing the metacharacter ;, runs two commands: the ping and the attacker's. Shells offer many such separators and substitutions (;, |, &&, backticks, $()), so the attacker has plenty of ways in. The root cause is identical to every injection class: untrusted data crossing into a command context. The moment user input can influence what commands run, not just their data, you have command injection.

// 02 Why it's so severe

Command injection is near the top of the severity scale because it grants the attacker the operating system itself. Once they can run OS commands, they can do anything the application's account can: read and exfiltrate files (configuration, credentials, private keys, source), install a backdoor or web shell for persistent access, pivot to other systems on the internal network, and ultimately take full control of the host. Because the injected commands run with the application's privileges, the outcome is typically complete compromise of the server and often a foothold into the wider environment - the same critical endpoint as insecure deserialization, SSTI and malicious file uploads, reached by yet another road. When a tester confirms command injection, it's an immediate critical-finding escalation.

// 03 Blind & out-of-band variants

The easy case returns the command's output in the response, so the attacker sees /etc/passwd right there. But many apps don't echo the output - that's blind command injection, and it's where weaker tests give up. Skilled testers don't, because the command still runs; you just can't see the result directly. Two techniques confirm and exploit it. Time-based: inject a command that sleeps for a few seconds and measure whether the response is delayed - a reliable proof of execution. Out-of-band (OAST): inject a command that makes the server contact an attacker-controlled server - a DNS lookup or HTTP callback - which proves execution and can even exfiltrate data by encoding it into the request. Blind variants are just as dangerous as visible ones; they're only harder to detect - which is precisely why a human tester with an OAST listener finds what a signature-matching scanner misses.

// 04 Where it hides

01

“Utility” features

Ping/traceroute tools, DNS lookups, file conversion, PDF generation, image processing - anything shelling out.

02

File & archive handling

Unzipping, virus scanning or processing uploads by invoking external command-line tools.

03

Admin & diagnostics

Backup, system-info and diagnostic functions that run OS commands under the hood.

04

Third-party integrations

Wrappers around CLI tools and legacy binaries the app drives via the shell.

The common thread: any feature whose implementation is “call an external program” is a candidate. A tester maps every such function and probes each with shell metacharacters and OAST payloads.

// 05 How to prevent it

The prevention hierarchy is clear. Best: don't call a system shell with user input at all. Most tasks people shell out for have a safe, built-in language API that does the operation directly - use it, and the vulnerability class simply can't exist. Where invoking an external program is genuinely unavoidable, use parameterised APIs that pass arguments as a separate array (not a single string), so the arguments are handed to the program without a shell interpreting them - no shell, no metacharacters, no injection. Add strict allow-list validation on any input that must reach the command (e.g. a value that can only be one of a fixed set). What doesn't work reliably is blocklisting dangerous characters: shells offer too many injection avenues to filter comprehensively, and bypasses are common. As with XXE and SSTI, safe-by-design beats filtering. Every web app engagement we run probes for command injection across all these vectors, per our methodology.

// 06 Frequently asked questions

What is command injection?

A vulnerability where an app passes untrusted input into a system shell command without proper handling, letting an attacker inject and execute their own OS commands on the server. It arises when developers build a shell command by concatenating user input, so the attacker adds shell metacharacters to append commands. Because they run with the app's privileges, it usually means arbitrary code execution and full compromise.

How does it lead to server compromise?

Once an attacker can run OS commands, they can do anything the app's account can: read and exfiltrate files including credentials, install a backdoor or web shell, pivot to other internal systems, and take full control. Because injected commands run with the app's privileges, the impact is typically complete host compromise and often a foothold into the wider environment - hence critical severity.

What is blind command injection?

When the app executes the injected command but doesn't return its output, so the attacker can't see the result directly. Testers confirm it with indirect techniques: a time delay via a sleep command, or out-of-band methods where the injected command makes the server contact an attacker-controlled server (DNS/HTTP), proving execution and even exfiltrating data. Blind variants are just as dangerous, only harder to detect - where manual testing outperforms scanning.

How do you prevent command injection?

Best: don't call a system shell with user input - use safe built-in language APIs that perform the operation directly. Where an external program is unavoidable, use parameterised APIs that pass arguments as a separate array so no shell interprets them, and apply strict allow-list validation. Filtering dangerous characters alone is unreliable; the durable fix is removing the shell from the equation or passing arguments safely.

// 07 Related reading

UG

Usama Gul

Founder & Penetration Testing Lead, CyberFortify

Chases command injection into the blind, out-of-band cases — proving execution with a timed sleep or an OAST callback — then prescribes the fix that removes the shell from the equation entirely.

App shelling out to the OS?

Any feature that calls an external program can be command-injectable. We probe every one — including the blind, out-of-band cases — prove the impact safely, and hand you the design fix.

Scope a web app test → Web app testing →