As the Web continues its march towards becoming the de facto interface for the world's software applications, developers must find effective ways to not only communicate with server processes such as MySQL, but also other operating system tools such as a shell or Ruby script. In this tutorial, I'll show you how to securely execute a variety of system-based commands via a PHP script, demonstrating how to build web applications that can tightly integrate with both the operating system and third-party software.

Minding the Security Risks

Hopefully, you understand how important it is to thoroughly validate all user input accepted through a web form; after all, if you neglect this task, your data could be stolen or damaged via a SQL injection attack, or your users could be spoofed via a cross-site scripting attack.
The risks of not properly vetting operating system commands initiated through a PHP script are no less severe! With this in mind, before jumping into a few examples it's worth taking a moment to understand how this particular feature can pose a significant security risk to not only your web site, but also to the stability of the entire web server.
When PHP is installed as an Apache module, it operates on behalf of the system user (which has assumed ownership of the Apache daemon). This means that when PHP executes an operating system command, the Apache daemon owner carries out that command. Therefore, any permissions assigned to the Apache daemon owner are applied when executing this command. Some improperly configured servers actually run Apache using the root user, meaning an errant PHP script could conceivably delete large parts of the operating system and any data stored within! Even in the more likely case of your server operating on behalf of a non-root user, the security risks remain nonetheless severe.
To illustrate this risk, consider that (as you'll soon learn) a PHP script is capable of executing a command that can read a file residing on the operating system, including those residing outside of the web document root. Suppose you're working as a software developer for a major university, and one day your supervisor asks you to create a web-based script that allows a student to retrieve information that verifies whether his or her student fee payment has been processed. This script would work by passing the student's social security number along as an argument to a command-line script, which in turn would communicate with a university mainframe. That command-line script would be executed like this:
%>/home/mainframe/fees.rb 123-45-6789
Your PHP script doesn't actually bother to verify whether the student input is a syntactically valid SSN, much less ensure that the SSN isn't accompanied by other input that could produce an unintended result, such as retrieving the server's /etc/passwd file (the file that contains the server's list of user accounts):
%>/home/mainframe/fees.rb 123-45-6789 ; cat /etc/passwd
If your PHP script were simply retrieving and displaying the returned output from presumably the fees.rb script, then this output would also include a list of all user accounts! Obtaining this list could eventually lead to an intruder surreptitiously entering the operating system due to a weak or missing account password. Even worse, if the web server daemon happened to be owned by the root user, then the intruder could—easily—modify or delete not only /etc/passwd, but also any other file residing on the server!
Clearly, you should avoid such gaffes at all costs. Thankfully, it's easy to avoid these sorts of security problems using native PHP syntax. I'll explain how in the remainder of this tutorial.

Executing a System Command

PHP actually supports several functions capable of executing a system command, including exec(), passthru(), shellexec(), and system(). Each of these functions exhibits a slightly different behavior:
Let's review a few examples involving these functions, beginning with the particularly simple task of executing the ls command in order to output a list of files residing in a particular directory:
<?php
  exec("ls -al /home/wjgilmore/pdfs", $lines);
  foreach($lines as $line) {
    echo "{$line}<br />";
  }
?>
Executing this script produces output identical to what you would see if executing ls -al /home/wjgilmore/pdfs from the command-line:
total 20
drwxr-xr-x 2 wjgilmore wjgilmore 4096 Jan 25 13:01 .
drwxr-xr-x 4 wjgilmore wjgilmore 4096 Jan 25 13:02 ..
-rw-r--r-- 1 wjgilmore wjgilmore 1366 Jan 25 13:00 01-2010.pdf
-rw-r--r-- 1 wjgilmore wjgilmore 1263 Jan 25 13:01 02-2010.pdf
-rw-r--r-- 1 wjgilmore wjgilmore 685 Jan 25 13:01 03-2010.pdf
Of course, there's nothing particularly insecure about this example because the system command has been hardcoded as an exec() parameter. However, let's return to the earlier scenario where a user was allowed to key in a Social Security Number, which was subsequently provided to a Ruby script. The user would presumably provide this input via a web form, with the SSN passed via the $_POST array. Coded in an insecure fashion, the script might look like this:
<?php

  $ssn = $_POST['ssn'];

  exec("/home/mainframe/fees.rb {$ssn}", $lines);

  foreach($lines AS $line) {
  
    echo "{$line}<br />";
  
  }

?>
To demonstrate the danger of this script, consider the following fees.rb script, which just outputs the SSN back to the user:
#!/usr/bin/ruby

ssn = ARGV[0]

puts "The SSN is #{ssn}\n"
Now, suppose the user entered the following "social security number" into the form:
123-45-6789 ; cat /etc/passwd
Providing this input to the PHP script results in the following output:
The SSN is 123-45-6789
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
...
Surely this isn't the output you intended!

Securing Your Scripts

Fortunately, several options are at your disposal for preventing such unintended consequences. The easiest solution involves using the escapeshellarg() and escapeshellcmd() functions to convert the user-supplied data into a safe format. The escapeshellarg() function will delimit the data with single quotes, as well as any single quotes already found in the data, thereby causing the data to be treated as a single argument. Therefore, the above malicious input would be converted to this:
'123-45-6789 ; cat /etc/passwd'
Once delimited, the PHP script will now produce the following output:
The SSN is 123-45-6789 ; cat /etc/passwd
The escapeshellcmd() function will escape any characters that could be used to trigger a system command. If applied to the above user-supplied data, the string will be converted to the following before being passed to the Ruby script:
123-45-6789 \; cat /etc/passwd
With the semi-colon escaped, the ensuing command can no longer be interpreted by the operating system.

Where to From Here?

Creating web-based applications that integrate tightly with the underlying operating system is pretty easy to do. However, you must be vigilant to avoid the serious security issues that can arise due to unchecked user input. Fortunately, PHP's native functionality makes it easy to vet user input in a way that greatly reduces the likelihood of stolen or damaged server data.
W. Jason Gilmore is founder of EasyPHPWebsites.com. He is the author of several popular books, including "Easy PHP web sites with the Zend Framework," "Easy PayPal with PHP," and "Beginning PHP and MySQL, Third Edition." to e-mail him.