Back to Blog
high SEVERITY8 min read

How Command Injection happens in PHP and how to fix it

A high-severity command injection vulnerability was discovered in `lib/Controller/Helper.php` where the `corruptline()` method used `exec()` to run sed and awk commands with user-controlled input. The fix replaced all shell command execution with native PHP file operations using `SplFileObject`, eliminating the command injection attack surface entirely.

O
By Orbis AppSec
Published August 22, 2026Reviewed August 22, 2026

Answer Summary

Command injection (CWE-78) in PHP occurs when user-controlled data is passed to shell execution functions like `exec()`, `shell_exec()`, or `system()`. In `lib/Controller/Helper.php`, the `corruptline()` method used `exec()` to run sed and awk commands with the `$zeile` (line number) parameter, which could be manipulated to inject arbitrary shell commands. The fix replaced all shell execution with native PHP file operations using `SplFileObject` and `fopen()`, eliminating the need for `escapeshellarg()` and removing the command injection risk entirely.

Vulnerability at a Glance

cweCWE-78 (OS Command Injection)
fixReplace shell execution with native PHP SplFileObject operations
riskArbitrary command execution on the server
languagePHP
root causeUsing exec() with sed/awk commands constructed from user-controllable input
vulnerabilityCommand Injection via exec()

Introduction

In a PHP application's lib/Controller/Helper.php file, we discovered a high-severity command injection vulnerability at line 257. The corruptline() method used exec() to run sed and awk shell commands for reading and replacing specific lines in log files. While the code attempted to use escapeshellarg() for protection, the presence of shell execution with user-controllable parameters created an unnecessary and dangerous attack surface. This vulnerability demonstrates why avoiding shell execution entirely is always preferable to relying on sanitization functions.

The vulnerable code pattern appeared in two locations within the same method: first when reading a line with sed, and again when replacing a line with a complex awk command. Both operations could be performed safely using native PHP file functions, making the shell execution completely unnecessary.

The Vulnerability Explained

The corruptline() method in lib/Controller/Helper.php was designed to read and replace specific lines in log files. Here's the vulnerable code that read a line:

if ($this->isExecAvailable()) {
    $fragment = exec("sed -n '{$zeile}p' " . escapeshellarg($file));
} else {
    // fallback code...
}

The critical issue is on line 257 where $zeile (a line number parameter) is directly interpolated into the sed command string: "sed -n '{$zeile}p' ". While $file is protected with escapeshellarg(), the $zeile variable is embedded directly into the command without any escaping.

The Attack Vector:

An attacker controlling the $zeile parameter could inject shell metacharacters to execute arbitrary commands. For example, if $zeile contained:

1'; whoami; echo '

The resulting command would become:

sed -n '1'; whoami; echo 'p' /path/to/file

This executes three separate commands: a sed command, whoami (or any malicious command), and an echo statement. The attacker gains arbitrary command execution with the privileges of the web server process.

The Second Vulnerability:

The same method contained an even more complex command injection point when replacing lines:

$cmd = sprintf(
    "awk -v n=%d -v f=%s 'NR==n{while((getline line < f)>0){print line}; close(f); next} {print}' %s > %s && mv %s %s",
    $zeile,
    escapeshellarg($tmpNew),
    escapeshellarg($file),
    escapeshellarg($file . '.tmp'),
    escapeshellarg($file . '.tmp'),
    escapeshellarg($file)
);
exec($cmd . ' 2>&1');

While this code uses %d for $zeile (which provides some type coercion), the complexity of the shell command chain with multiple file operations, redirections, and the && operator creates multiple potential injection points. The use of sprintf() with %d offers minimal protection if $zeile contains non-numeric data or if PHP's type juggling allows unexpected values through.

Real-World Impact:

In this application, the corruptline() method likely handles log file manipulation. An attacker who could control the line number parameter (perhaps through a log viewing interface or API endpoint) could:

  1. Execute arbitrary system commands
  2. Read sensitive files from the server
  3. Modify or delete critical files
  4. Establish persistent backdoors
  5. Pivot to other systems on the network

The vulnerability is particularly dangerous because log viewing functionality is often exposed to authenticated users, and developers may not consider line numbers to be security-sensitive input.

The Fix

The fix eliminates shell execution entirely by replacing sed and awk with native PHP file operations. Here's the before and after comparison for reading a line:

Before (Vulnerable):

if ($this->isExecAvailable()) {
    $fragment = exec("sed -n '{$zeile}p' " . escapeshellarg($file));
} else {
    try {
        $fileObj = new \SplFileObject($file);
        $fileObj->seek($zeile - 1);
        $fragment = $fileObj->current();
    } catch (\Exception $e) {
        $fragment = 'could not read line';
    }
}

After (Secure):

try {
    $fileObj = new \SplFileObject($file);
    $fileObj->seek($zeile - 1);
    $fragment = $fileObj->current();
} catch (\Exception $e) {
    $fragment = 'could not read line';
}

The fix makes the fallback code path the primary implementation. SplFileObject is a native PHP class that provides object-oriented file access without any shell involvement. The seek() method jumps directly to the specified line number, and current() returns that line's content. Critically, no shell metacharacters can be injected because no shell is ever invoked.

For the line replacement operation:

Before (Vulnerable):

if ($this->isExecAvailable()) {
    $tmpNew = tempnam(sys_get_temp_dir(), 'newline_');
    file_put_contents($tmpNew, $replaceWith . PHP_EOL);

    $cmd = sprintf(
        "awk -v n=%d -v f=%s 'NR==n{while((getline line < f)>0){print line}; close(f); next} {print}' %s > %s && mv %s %s",
        $zeile,
        escapeshellarg($tmpNew),
        escapeshellarg($file),
        escapeshellarg($file . '.tmp'),
        escapeshellarg($file . '.tmp'),
        escapeshellarg($file)
    );
    exec($cmd . ' 2>&1');
    @unlink($tmpNew);
} else {
    $tempFile = $file . '.tmp';
    $handleIn = fopen($file, 'r');
    $handleOut = fopen($tempFile, 'w');
    // ... line-by-line copy and replace logic
}

After (Secure):

$tempFile = $file . '.tmp';
$handleIn = fopen($file, 'r');
$handleOut = fopen($tempFile, 'w');
// ... line-by-line copy and replace logic

Again, the fix removes the entire shell execution branch and uses only the native PHP implementation. The code now uses fopen() to read the original file and write to a temporary file, copying lines one by one and replacing the target line when encountered. This approach:

  1. Eliminates command injection risk - No shell is ever invoked
  2. Removes unnecessary complexity - The awk command with redirections and chained commands is replaced with straightforward PHP
  3. Improves portability - No dependency on sed/awk being available
  4. Maintains functionality - The behavior is identical for all valid inputs

The security improvement is absolute: there is no longer any code path that constructs shell commands from user input, making command injection impossible in this method.

Prevention & Best Practices

1. Avoid Shell Execution Entirely

The most effective prevention is to never invoke a shell when native language functions exist. In PHP:

  • Use SplFileObject, fopen(), fread(), file(), file_get_contents() instead of sed/grep/awk
  • Use glob() instead of shell wildcards
  • Use PHP's file system functions instead of ls, find, cp, mv

2. Validate Input Strictly

When you must accept user input that influences program behavior:

  • Use allowlists, not denylists
  • Validate data type (use type declarations in PHP 7+)
  • Check ranges for numeric input
  • Use regex patterns for structured data

For the $zeile parameter, proper validation would be:

public function corruptline(?int $zeile, ?string $wtlog): array {
    if ($zeile === null || $zeile < 1) {
        throw new \InvalidArgumentException('Line number must be a positive integer');
    }
    // ... rest of method
}

3. Defense in Depth

If shell execution is unavoidable:

  • Use escapeshellarg() on ALL parameters
  • Use escapeshellcmd() on the entire command
  • Never use shell_exec() or backticks with user input
  • Consider using proc_open() with explicit argument arrays instead of shell strings

4. Static Analysis Integration

Tools like Semgrep can detect command injection patterns automatically. The rule php.lang.security.exec-use.exec-use specifically identifies:

  • Calls to exec(), shell_exec(), system(), passthru(), popen()
  • Non-constant command strings
  • Missing or insufficient sanitization

Integrate static analysis into your CI/CD pipeline to catch these issues before they reach production.

5. Security Standards

This vulnerability maps to:

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command
  • OWASP A03:2021: Injection
  • SANS Top 25: CWE-78 is ranked #7 in most dangerous software weaknesses

Key Takeaways

  • The corruptline() method in Helper.php used exec() with sed/awk to perform file operations that could be done safely with native PHP functions, creating an unnecessary command injection risk.

  • Direct interpolation of $zeile into the sed command ("sed -n '{$zeile}p' ") allowed shell metacharacter injection despite escapeshellarg() being used on other parameters.

  • SplFileObject and fopen() provide safer alternatives to shell-based file manipulation, eliminating the attack surface entirely rather than relying on sanitization.

  • The fix removed all shell execution from the method, proving that the original implementation's complexity was unnecessary and the fallback code path was actually the better solution.

  • Even with escapeshellarg(), shell execution creates exploit primitives that automated attack tools can potentially chain with other vulnerabilities, making complete avoidance the best practice.

How Orbis AppSec Detected This

  • Source: The $zeile parameter passed to the corruptline() method, which could originate from HTTP requests or other untrusted sources
  • Sink: exec() calls at line 257 and subsequent lines in lib/Controller/Helper.php where sed and awk commands are constructed and executed
  • Missing control: The $zeile parameter was directly interpolated into shell commands without validation or escaping, and shell execution was used unnecessarily
  • CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
  • Fix: Removed all shell execution paths and replaced them with native PHP file operations using SplFileObject and fopen()

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This command injection vulnerability in lib/Controller/Helper.php demonstrates a critical security principle: the best defense is elimination, not sanitization. While escapeshellarg() provides some protection, the presence of shell execution with user-controllable parameters creates unnecessary risk. By replacing sed and awk with native PHP file operations, the fix not only eliminates the vulnerability but also simplifies the code and removes external dependencies.

For developers working on similar code, the lesson is clear: always prefer native language functions over shell execution. When manipulating files, reading lines, or performing common operations, modern programming languages provide safe, efficient alternatives that don't require invoking a shell. This approach eliminates entire classes of vulnerabilities and makes your code more portable and maintainable.

References

Frequently Asked Questions

What is command injection in PHP?

Command injection occurs when untrusted data is passed to shell execution functions (exec, shell_exec, system, passthru) without proper sanitization, allowing attackers to execute arbitrary system commands. Even with escapeshellarg(), the pattern creates unnecessary risk.

How do you prevent command injection in PHP?

Avoid shell execution entirely by using native PHP functions. For file operations, use SplFileObject, fopen/fread, or file_get_contents instead of sed/awk. If shell execution is unavoidable, use escapeshellarg() on all parameters and validate input against strict allowlists.

What CWE is command injection?

Command injection is classified as CWE-78 (Improper Neutralization of Special Elements used in an OS Command). It's part of the OWASP Top 10 (A03:2021 – Injection) and can lead to complete system compromise.

Is escapeshellarg() enough to prevent command injection?

While escapeshellarg() provides defense-in-depth, it's not foolproof and creates an unnecessary attack surface. The best practice is to avoid shell execution entirely by using native language functions. This eliminates the risk at its source rather than relying on sanitization.

Can static analysis detect command injection?

Yes, tools like Semgrep can detect command injection patterns by tracking data flow from user input to dangerous sinks like exec(). The rule php.lang.security.exec-use.exec-use specifically identifies non-constant commands passed to execution functions, flagging potential injection points.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #62

Related Articles

critical

How SQL Injection happens in PHP PDO queries and how to fix it

A critical SQL injection vulnerability was discovered in the `getOfficialContests()` method of ContestRepository.php, where the `$site_id` parameter was directly interpolated into a SQL query string instead of using prepared statements. This vulnerability allowed attackers to inject arbitrary SQL commands and potentially access or manipulate the entire contest database. The fix replaced `pdo->query()` with `pdo->prepare()` and proper parameter binding.

critical

How SQL injection happens in PHP MySQLi and how to fix it

A critical SQL injection vulnerability was discovered in `sign_up.php` where user registration inputs—including Username and Email—were directly concatenated into SQL queries. Despite using `mysqli_real_escape_string()`, the code remained exploitable. The fix replaces all string-concatenated queries with MySQLi prepared statements and bound parameters, completely eliminating the injection vector.

critical

How session fixation happens in PHP OAuth login handlers and how to fix it

A session fixation vulnerability in `trunk/web/login_weibo.php` allowed attackers to hijack authenticated user sessions by pre-setting a victim's PHP session ID before they logged in via Weibo OAuth. The fix was a single, critical call to `session_regenerate_id(true)` inserted immediately before assigning the authenticated user's ID to the session. Left unpatched, this vulnerability could have given an attacker full access to any account whose Weibo OAuth login they could observe or influence.

critical

How Telegram OAuth hash validation bypass happens in PHP and how to fix it

A critical vulnerability in the Telegram OAuth provider allowed attackers to bypass authentication by submitting fabricated hash values without cryptographic validation. The fix replaces an unsafe string comparison with PHP's constant-time `hash_equals()` function, preventing timing attacks and ensuring all user data parameters are properly validated against the HMAC-SHA256 signature.

high

How Regular Expression Denial of Service happens in JavaScript and how to fix it

CVE-2026-33671 is a Regular Expression Denial of Service (ReDoS) vulnerability in the picomatch glob-matching library, triggered by specially crafted extglob patterns that cause catastrophic regex backtracking. The fix upgrades picomatch to version 4.0.4 (with overrides pinning all transitive copies) in the client's dependency tree, eliminating the vulnerable regex evaluation path. Left unpatched, any code path that passes user-influenced glob patterns to picomatch could be weaponized to stall a

high

How insecure string copy functions happen in C and how to fix them

A high-severity buffer overflow risk was discovered in `login/main.c` where `strcpy()` was used to copy the `HOME` environment variable into a fixed-size 512-byte buffer without any bounds checking. An attacker controlling the `HOME` environment variable could overflow `pwd_file_name`, potentially corrupting memory or hijacking execution. The fix replaces the two-step `strcpy`/`strcat` pattern with a single, bounds-safe `snprintf` call.