External programs are commonly invoked to perform a function required by the overall system. This is a form of reuse and might even be considered a crude form of component-based software engineering. Command and argument injection vulnerabilities occur when an application fails to sanitize untrusted input and uses it in the execution of external programs.

The exec() built-in function is the standard mechanism for executing system commands; it is a wrapper around the POSIX exec family of system calls. The system() built-in function is similar to exec, but it takes a single string, whereas exec()takes a list. The qx operator, often represented by encasing a command in backquotes (``), can also be used to execute an arbitrary command. Finally, the open() function can also execute commands in a subprocess and either send data to them or fetch data from them (but not both).

Command injection attacks cannot succeed unless a command interpreter is explicitly invoked. However, argument injection attacks can occur when arguments have spaces, double quotes, and so forth, or when they start with a - or / to indicate a switch.

This rule is a specific instance of IDS33-PL. Sanitize untrusted data passed across a trust boundary. Any string data that originates from outside the program's trust boundary must be sanitized before being executed as a command on the current platform.

Noncompliant Code Example (open())

This noncompliant code example tries to list a directory specified by the user. It safely uses the three-argument open() command, as required by IDS31-PL. Do not use the two-argument form of open().

my $dir = $ARGV[0];
open( my $listing, "-|", "ls -F $dir") or croak "error executing command: stopped";
while (<$listing>) {
  print "Result: $_";
}
close( $listing);

The program also works properly when given a valid directory as an argument:

% ./sample.pl ~
Result: bin/
Result: Desktop/
Result: src/
Result: workspace/
%

But it can also have unintended consequences, as in this case, if an attacker injects an arbitrary command to be executed by the call to open():

% ./example.pl "dummy ; echo bad"
ls: cannot access dummy: No such file or directory
Result: bad
% ./example.pl 

Compliant Solution (Sanitization)

This compliant solution sanitizes the untrusted user input by permitting only a small group of whitelisted characters in the argument that will be passed to open(); all other characters are excluded.

my $file;
my $dir = $ARGV[0];
croak "Argument contains unsanitary characters, stopped" if ($dir =~ m|[^-A-Za-z0-9_/.~]|);
open( my $listing, "-|", "ls -F $dir") or croak "error executing command: stopped";
while (<$listing>) {
  print "Result: $_";
}
close( $listing);

This code properly rejects shell commands:

% ./example.pl "dummy ; echo bad"
Argument contains unsanitary characters, stopped at ./example.pl line 8
% 

However, this code also rejects valid directories if they contain characters not in the whitelist regex.

Compliant Solution (Shell Avoidance)

This compliant solution again sanitizes the untrusted user input. However, it uses the multi-arg form of open().

my $file;
my $dir = $ARGV[0];
croak "Argument contains unsanitary characters, stopped" if ($dir =~ m|[^-A-Za-z0-9_/.~]|);
open( my $listing, "-|", "ls", "-F", $dir) or croak "error executing command: stopped";
while (<$listing>) {
  print "Result: $_";
}
close( $listing);

The perlfunc manpages states, regarding all but the first two arguments to open():

If there is only one scalar argument, the argument is checked for shell metacharacters, and if there are any, the entire argument is passed to the system's command shell for parsing (this is "/bin/sh -c" on Unix platforms, but varies on other platforms). If there are no shell metacharacters in the argument, it is split into words and passed directly to "execvp," which is more efficient.

So this form of open() is preferable if your platform's shell might be set up incorrectly or maliciously.

Compliant Solution (Restricted Choice)

This compliant solution prevents command injection by passing only trusted strings to open(). The user has control over which string is used but cannot provide string data directly to open().

my %choices = (BIN => "~/bin",
               LIB => "~/lib",
               SRC => "~/src");
my $choice = $choices{$ARGV[0]};
croak "Invalid argument, stopped" if (!defined $choice);
open( my $listing, "-|", "ls -F $choice") or croak "error executing command: stopped";
while (<$listing>) {
  print "Result: $_";
}
close( $listing);

This compliant solution hard codes the directories that may be listed.

This solution can quickly become unmanageable if you have many available directories. A more scalable solution is to read all the permitted directories from an external file into a hash object, and the external file must be kept secure from untrusted users.

Compliant Solution (Avoid Interpreters)

When the task performed by executing a system command can be accomplished by some other means, it is almost always advisable to do so. This compliant solution uses the opendir(), readdir(), and closedir() subroutines to provide a directory listing, eliminating the possibility of command or argument injection attacks.

my $dir = $ARGV[0];
opendir( my $listing, $dir) or croak "error executing command: stopped";
while (readdir($listing)) {
  print "Result: $_\n";
}
closedir($listing);

Noncompliant Code Example (VU#583020)

US-CERT Vulnerability #583020  describes Perl code that invoked the system() built-ig function without sanitizing its argument:

sub do {
        shift;
        $do_call = "xmms -" . shift;
        system $do_call;
        return $do_call;
  }

An attacker who could control the arguments to the do() subroutine could cause the code to invoke arbitrary shell commands. This code also violates DCL31-PL. Do not overload reserved keywords or subroutines.

Compliant Solution (VU#583020)

This code was mitigated by adding a regex to make sure that only a single character supplied by the user could be added to the $do_call variable before passing it to system().

sub do {
    shift;
    $command = shift;
    $command =~ /([\w])/;
    $command = $1;
    $do_call = "xmms -" . $command;
    system $do_call;
    return $do_call;
  }

This code still violates DCL31-PL. Do not overload reserved keywords or subroutines; it is shown here for historical accuracy.

Risk Assessment

Using deprecated or obsolete classes or methods in program code can lead to erroneous behavior.

Recommendation

Severity

Likelihood

Remediation Cost

Priority

Level

IDS34-PL

High

Probable

Medium

P12

L1

Automated Detection

Tool

Diagnostic

Taint modeInsecure dependency in (system|piped open)

Related Guidelines

Bibliography

 

 


6 Comments

  1. Peter Makholm says

    My preferred solution would be to use the more-than-three arguments form of open(). This way open() will bypass using the system shell to interprete the command:

    open( my $listing, "-|", "ls", "-F", $dir ) or croak("error executing command: $!");
    

    This is equivalent to the argument processing for system() which might be a bit better documented. This would work for your VU#583020 examples:

    system( "xmms", "-$command" );
    

    (BTW: these examples are not compliant with the recommendations of DCL06-PL. You might want to call the function do_play or something).

    But when it is possible to restate the problem to not cross a trust boundary as in your "Avoid interpreters" solution this is fine to. But it only works for the simple commands which are feasible to reimplement directly in your own code.

    1. I added your version of the solution using a 5-arg open as another compliant solution. I also noted the noncompliance of the VU#583020 code samples wrt DCL31-PL.

  2. Hi David,

      You seem to have forgotten the qx operator, which is the equivalent to backticks which calls execve().

    Examples: 1) qx/id/

                    2) qx " id "

                    3) qx ] id ]

                    ie: perl -e 'print qx/id/'

    and so forth.

    When auditing be careful not to simply regex for "qx/" as for that is not sufficient.  There are several ways in which the operator can withstand different syntax when used.

    1. I added a sentence about qx() to the intro section, thanks for  catching this oversight.

      1. Hi David,

          My deepest apologies – I'm not trying to nitpick but qx should be mentioned as an operator and not a function.  If it's left as noted above, people will scan for qx as a function when auditing and it will lead to massive false-negatives (which is why I left several examples on the different ways to use the operator which may deter auditors from picking it up properly when scanning source code).

        Thanks for keeping up with these documents by the way, I was happy I stumble upon them -- they're a great resource of information.

        1. Agreed. It is listed under perlfunc(1), but referred to elsewhere as an operator. Updated the intro.