Security Tip: Watch Out for Command Injection

[Tip#57] You've heard about SQL Injection and Cross-Site Scripting but what about another big injection avenue: Command Injection? It's less common but just as critical that you're aware of it...

Security Tip: Watch Out for Command Injection

Greetings my friends! After last week’s hard to exploit tip about comparing strings, we're focusing on a super practical and incredibly important attack vector this week: Command Injection! This is a less-common vulnerability due to it’s nature, but incredibly important to be aware of, as it can give an attacker full access to your server…

Quick note about my Laravel Security Audits and Penetration Tests: I’m reducing my availability next year, so please reach out if you’re thinking about an audit in 2024, so I can reserve you time in my schedule1. 🕵️

Don’t forget, you can find me on most of the social platforms at src.id.au/links, and I have a few Bluesky and T2/Pebble invites, if you’re looking for one. 😉


Watch Out for Command Injection

Command Injection is a type of Injection Attack, where a system command is vulnerable to injection through user input. Like SQL Injection, carefully crafted user input can be used to escape out of the intended command to perform other operations on the system. This can be used for simple things like reading sensitive files (like the .env), modifying the code, or accessing other files of the server outside the application2, or for starting up a remote connection to allow the attacker direct access to the system! If this happens, you’ve lost control over your server, and it can be an incredibly difficult process getting the attacker back out again…

Command Injection occurs when you do something like this in your code:

use Illuminate\Support\Facades\Process;
 
public function export(Request $request)
{
    $export = $request->get('export');

    $result = Process::run("/path/to/command export='{$export}'");

    return $result->output();
}

It looks pretty innocent at face value, but it’s easy to overlook the fact that we’re pulling in user input, and as I’ve said many times before: Don’t Trust User Input!3

There isn’t some magical method you can pipe your input into, as it depends what your command requires, but you can’t just pass it through as-is! You need to figure out the best escaping method for your output and apply it to the input. Also, don’t just do the bare minimum here - try to validate, sanitise, and escape the input as much as possible without breaking the value, so you can use it safely.

For example, if we look at the above code, the input is surrounded by single quotes, so removing or escaping the single quotes should work at a minimum, but you could pass it through Laravel’s escaping `e()` method to apply HTML escaping to quotes. Or use something like `str_replace(['"', "'"], '', $export)` to get rid of quotes.

Unlike that example, some values won’t be inside quotes, and you need to be even more careful. Even a single space can be used to inject different commands or add options, hijacking the command.

Here’s a good starting point for protecting your commands from injection through user input:

Do you know all possible values?
If so, ensure the provided value matches an expected value through input validation.

use Illuminate\Support\Facades\Process;
use Illuminate\Validation\Rule;

public function export(Request $request)
{
    $data = $request->validate([
        'export' => [
            'required',
            Rule::in(['pages', 'chapters']),
        ]
    ]);

    $result = Process::run("/path/to/command export={$data['export']}");

    return $result->output();
}

Can the value have limited characters?
If the value has a limited character set, you can easily validate or strip out the extra characters to ensure it’s safe.
(Limiting values to alpha-numeric is always a good option, if you can do it!)

use Illuminate\Support\Facades\Process;
use Illuminate\Validation\Rule;

public function export(Request $request)
{
    $data = $request->validate([
        'export' => ['required', 'alpha_num:ascii'],
    ]);

    $result = Process::run("/path/to/command export={$data['export']}");

    return $result->output();
}

Can you wrap the value in quotes?
If the command supports wrapping the value inside quotes, you can use these to prevent special characters (or even spaces) from modifying the command or chaining on other commands. Just make sure the value itself doesn’t contain any quotes.

use Illuminate\Support\Facades\Process;

public function export(Request $request)
{
    $export = $request
        ->string('export')
        ->replace(['"', "'"], '');

    $result = Process::run("/path/to/command export='{$export}'");

    return $result->output();
}

Hopefully that gives you a good starting point!

The takeaway from this is that, if you’re generating commands that include user-input, you need to be very careful about the values you accept and pass into the commands. Command Injection is a huge risk and easy to fall into if you’re not careful.

If you’ve needed to pass user input into commands, what methods have you used to prevent Command Injection?

UPDATE: So I completely forgot about the incredibly useful PHP core function `escapeshellarg()`, which is designed for escaping user input for use on the command line. I'd still recommend validating and limiting the input at much as possible, but you should definitely use this function to escape values. Check it out at: https://www.php.net/manual/en/function.escapeshellarg.php

Thanks to @JackWH21 on Twitter for the reminder!


Looking to learn more?
Security Tip #38: Timebox for Timing Attacks
▶️ In Depth #13: Stealing Password Tokens with Forwarded Host Poisoning

  1. I will be booking in past clients first, and then I would love to give Securing Laravel subscribers second preference before opening up to new clients.

  2. Such as /etc/passwd and /etc/shadow, or other apps running on the same server!

  3. I even wrote a post on Laravel News about this!