PHP Application Security

Will Bond

May 25th, 2011

Introduction

Hi, I’m Will. http://wbond.net

I’m the director of engineering at iMarc. http://imarc.net

I work on Flourish. http://flourishlib.com

The Concept

Being aware of security vulnerabilities is paramount to writing secure code.

Why Security is Important

Web Security Concerns

  1. Operating system, server software
  2. Application code
  3. Security testing

Time & Difficulty Impact

PHP Security Topics

Character Encodings

There are edge-case vulnerabilities that are possible when using different character encodings.

Always create strictly valid output and clean all input. You should use UTF-8 everywhere: HTML, database, text files, APIs, javascript, email, URLs, etc.

Character Encoding

Overview
When a character encoding is not specified in the Content-Type: HTTP header or the HTML <meta> tag
Vulnerability
Can allow for UTF-7 XSS, SQL injection
Prevention
Explicitly set encoding in Content-Type: header and clean input values for invalid characters — you should use UTF-8 for everything

Character Encoding Code

Before any output is created, be sure to set the Content-Type: HTTP header with the encoding you are using.

header('Content-type: text/html; charset=utf-8');

The iconv extension and function can be used to remove invalid characters.

$name = iconv('UTF-8', 'UTF-8//IGNORE', $_POST['name']);

Character Encoding Code (Cont)

The iconv extension and function can also be used to convert between different encodings.

$name = iconv('Windows-1252', 'UTF-8', $name);

Be sure to specify the default character encoding when creating a database.

-- MySQL
CREATE DATABASE example CHARACTER SET 'utf8';
-- PostgreSQL
CREATE DATABASE database_name ENCODING = 'UTF-8';

And for the connection when running queries.

-- MySQL
SET NAMES 'utf8';
SET CHARACTER SET utf8;
-- PostgreSQL
SET NAMES 'UTF8';

User Input

Overview
Data coming from users should never be trusted. This includes $_GET, $_POST, $_REQUEST, $_COOKIE, $_FILES, $_SERVER, web services and external APIs.
Vulnerability
Various – we’ll be talking specifics next
Prevention
Use the filter extension (PHP 5.2+) or manually type-cast, whitelist and escape.

User Input Code

// PHP filter extension
$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_SPECIAL_CHARS);
echo $name;
 
// Manual escaping
$name = isset($_POST['name']) ? $_POST['name'] : NULL;
echo htmlspecialchars($name, ENT_COMPAT, 'UTF-8');
 
// Flourish escaping
$name = fRequest::get('name');
echo fHTML::encode($name);

Register Globals

Overview
A PHP ini setting that creates variables from all of the keys in $_GET, $_POST, $_REQUEST, $_COOKIE, $_FILES and $_SERVER.
Vulnerability
It is easy to allow unfiltered user input through, makes maintenance more difficult
Prevention
Set the register_globals ini setting to 0 – this is the default for PHP 5

Magic Quotes

Overview
A PHP ini setting that tries to sanitize user input by prefixing all ', ", \ and null byte characters with a backslash.
Vulnerability
It creates a false sense of security and produces input that is invalid for many tasks.
Prevention
Set the magic_quotes_gpc and magic_quotes_runtime ini settings to 0 – this is the default for PHP 5

Cross-Site Request Forgeries

Also commonly known as CSRF or XSRF

Overview
A request from a malicous site that uses a visitor’s authenticated state to perform unauthorized operations
Vulnerability
A user’s logged-in state is shared across browser tabs, which opens the possibility for another site to force or trick a user into performing an operation they didn’t intend
Prevention
Use the HTTP POST method for any actions other than viewing a page, require authentication tokens be sent as a request parameter

CSRF Code

<?php
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
	if (!isset($_SESSION['request_token']) || 
		$_SESSION['request_token'] != $_POST['request_token']) {
		// Error
	}
	
} else {
	// Create a random string and save it in the session for
	// verification upon post
	$token = fCryptography::randomString(32);
	$_SESSION['request_token'] = $token;
}
?>
<form method="post" action="">
    <input type="hidden" name="request_token" value="<?= $token ?>" />
</form>

Request Value Fixation

Overview
When the $_REQUEST superglobal is used and a cookie has the same name as a GET or POST parameter
Vulnerability
PHP uses the variables_order ini setting to populate $_REQUEST. It defaults to EGPCS, which means a value from a cookie will override a value from GET or POST.
Prevention
Use the most specific superglobal: $_GET, $_POST or $_COOKIE. In PHP 5.3, the request_order ini setting exists.

Cross-Site Scripting

Also commonly known as XSS

Overview
Letting a visitor write raw HTML that will be included in HTML output
Vulnerability
Can be used to steal session cookies and run arbitrary scripts in browser
Prevention
HTML Purifier (http://htmlpurifier.org), htmlspecialchars()

XSS Code

Echoing user input into HTML creates XSS vulnerabilities

Welcome <?= $_POST['name'] ?>!

A malicious user could inject javascript that could read the visitors cookies

Welcome <script type="text/javascript">
/* Code to read cookies */
</script>

The simplest protection is to use htmlspecialchars() which will encode the <, >, ", ' and & characters, preventing the user from creating HTML tags.

Welcome <?= htmlspecialchars($_POST['name'], ENT_COMPAT, 'UTF-8') ?>!

SQL Injection

Overview
Visitor input that is concatenated or inserted into a string containing a SQL query
Vulnerability
Attackers can manipulate database contents, escalate their authentication level
Prevention
Use prepared statements or type casting/filtering & escaping

SQL Injection Code

Simply concatenating input to a SQL string will allow an attacker to run commands in your database.

$query = "SELECT * FROM users WHERE user_id = " . $_POST['user_id'];

This is the SQL that would be run if name was set to 1; DELETE FROM users;

SELECT * FROM users WHERE user_id = 1; DELETE FROM users;

SQL Injection Code (Cont)

The prepared statement APIs for the various database extensions vary greatly, however here is an example of using type-casting and Flourish’s database layer.

// Simple casting
$query = "SELECT * FROM users WHERE user_id = "
	  . intval($_POST['user_id']);
 
// Using Flourish’s fDatabase class
$result = $db->query(
	"SELECT * FROM users WHERE user_id = %i",
	$_POST['user_id']
);

Email Injection

Overview
When a visitor can input content that is placed into email headers, such as the From: header
Vulnerability
Using fetures of the MIME email standard, malicious users can embed spam messages in the original email headers
Prevention
Remove newlines from any user input being used anywhere but the email body. Validate/filter email addresses with filter extension or fEmail.

Email Injection Code

Not filtering valuese before creating a From: header creates an email injection vulnerability.

$headers = "From: " . $_POST['email'] . "\r\n";

The following header could be created by posting an email of \r\nSubject: Spam Message\r\n\r\nThe spam body

From: john@example.com
Subject: Spam Message

The spam body

Email Injection Code (Cont)

The following techniques can prevent email injection.

// Simple replacement of newlines
$email = str_replace(array("\r", "\n"), '', $_POST['email']);
 
// Using the filter extension
$email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);

File Uploads

Overview
When allowing file uploads, don’t trust user input
Vulnerability
A user could upload a PHP file or other executable script. Mime types reported in the $_FILES superglobal can be faked.
Prevention
Delete uploaded files with extensions such as .php, or any other files that can affect the functionality of your web server like .htaccess. Check mime types on the server using finfo_file() or fFile.

Other Types of Injection

Overview
Any place commands include user input, injection is possible. This includes shell commands, LDAP operations and XPath queries.
Vulnerability
Attackers can cause unintended side-effects, or access resources that should not be accessible.
Prevention
Filter user input, preferrably via a white-list

Password Hashing

Overview
Any site the provides user login functionality that relies on passwords
Vulnerability
If passwords are stored in plaintext, or even with trivial hashing, hackers could access users’ other web/computer accounts if they obtain read access to the user database
Prevention
Use crypt() with a Blowfish cipher salt, or thousands of rounds of a random salt combined with the sha1() hash

Password Hashing Code

fCryptography provides a function that runs 1000 iterations of sha1() over a random salt concatenated with the output of the last iteration.

$hashed_password = fCryptography::hashPassword($password);

The result is a formatted string containing a fingerpring, the salt and the hash.

fCryptography::password_hash#Gu19bpZN94#ac74c4ad9ed7103e051e583af86599b95237e9af

This technique is used to be compatible with architectures that did not have Blowfish cipher support before PHP 5.3. It is loosely based on the MD5 password hashing algorithm in FreeBSD written by Poul-Henning Kamp.

Password Hashing Code (Cont)

The generally accepted best practice is to use crypt(), which uses the Blowfish cipher to hash a password. It has a purposefully slow key setup.

// Blowfish-based crypt method - may not work on all OSes before PHP 5.3
$salt = '$2a$08$' . fCryptography::randomString(22, 'alphanumeric');
$hash = crypt($password, $salt);
$hashed_password = $salt . '||' . $hash;

Session Fixation

Overview
If the session identifier can be manipulated to provide a known, authenticated session to an attacker
Vulnerability
The session ID ties a user’s data in the $_SESSION superglobal to their browser. If an attacker can get a legitimate user to log in using a known session ID, the attacker then gains access to the user’s account.
Prevention
Only allow session IDs to be provided through cookies, by setting the session.use_only_cookies ini value to 1. Regenerate session IDs upon privilege escalation with session_regenerate_id().

Session Fixation Example

Phisher sends url to user via official-looking email

http://mybanksite.com/login?PHP_SESSID=8372AE3B210FC7D9827A

User logs into their bank account, then hacker users same URL to access their session.

The session.use_only_cookies ini setting would tell PHP to ignore the session ID since it is in the URL.

Sidejacking

Overview
When a session identifier is intercepted and used to gain access to a user’s account
Vulnerability
The session ID can be intercepted in transit if transfered over a non-secure connection. Biggest vulnerability is on open wifi networks.
Prevention
Only allow session ID cookie to be sent over SSL by setting the session.cookie_secure ini value to 1. The session.cookie_httponly ini setting can help protect against JS accessing session cookies in the case of XSS, however it is not supported by all browsers.

Cross-Site Session Transfer

Overview
When the directory storing session files is shared between multiple sites
Vulnerability
If code is similar between sites, attackers can log into Site A and then use that session ID with Site B, giving them all of the privileges they have on Site A.
Prevention
Have each site use a distinct session directory by calling session_save_path(). Optionally encrypt a token in the session.

XSST Example

User logs into site A as administrator, looks at session cookie and copies session ID ABD2782F97E0280ABC.

Session file is saved in /tmp/sess_ABD2782F97E0280ABC.

Site B is set to use /tmp as the session save path also, so user creates a session ID cookie for site B using his session ID from site A and can now view pages on site B.

Path Traversal

Overview
When user input it used to read or manipulate the file system
Vulnerability
Attackers can read files they shouldn’t have access to by using parent directory traversal ../ or alternate encodings, such as hex.
Prevention
Use realpath() to get the canonical path and compare with a whitelist

Path Traversal Example

Combining user input into a file path creates a traversal vulnerability.

$image = '/path/to/image/dir/' . $_GET['file'];

An attacker could use ../ to move up any number of directories and read the contents of sensitive files.

/path/to/image/dir/../../../../db.config

realpath() return the canonical path, allowing for simple comparisons.

$image = realpath('/path/to/image/dir/' . $_GET['file']);
if (strpos('/path/to/image/dir/', $image) !== 0) {
	// Invalid file requested
}

Path Disclosure

Overview
When error messages are shown to visitors that include file system paths
Vulnerability
Can be used with Path Traversal to access files, can be used to detect library/app versions and thus vulnerabilities
Prevention
Email errors with set_error_handler() or log errors via the log_errors ini setting. In both cases, set the display_errors ini setting to 0 on production servers.

Pseudo-Random Number Generator

Overview
If any PHP (anywhere on the server) tries to seed the PRNG via srand() or mt_srand()
Vulnerability
Predictable seeds allow for attackers to determine “random” values, such as password reset codes
Prevention
Use Suhosin patch (http://hardened-php.net/suhosin/) if possible, openssl_random_pseudo_bytes() (5.3+), or explicitly set seed with real random data from /dev/random on *nix/BSD or CAPICOM.Utilities.1 GetRandom method on Windows.

PRNG Example

// For non-shared hosts, don’t seed the PRNG!
 
// If you are on a shared *nix host, this is the best seed
$handle = fopen('/dev/urandom', 'rb');
$bytes = fread($handle, 4);
fclose($handle);
 
// Shared Windows host
$capi  = new COM('CAPICOM.Utilities.1');
$bytes = base64_decode($capi->getrandom(4, 0));
unset($capi);
 
$seed = (int) (base_convert(bin2hex($bytes), 16, 10) - 2147483647);
mt_srand($seed);

Cryptography

Overview
It is easy to write insecure code when encrypting data via the mcrypt extension
Vulnerability
A poor block cipher mode or cipher choice, or an implementation mistake could cause data to be easily decrypted.
Prevention
Use proven cryptographic code (such as a library) or learn as much as you can about cryptography

Where Can I Learn More?