PHP Application Security

Will Bond

May 25th, 2011


Hi, I’m Will.

I’m the director of engineering at iMarc.

I work on Flourish.

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

When a character encoding is not specified in the Content-Type: HTTP header or the HTML <meta> tag
Can allow for UTF-7 XSS, SQL injection
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
-- PostgreSQL

And for the connection when running queries.

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

User Input

Data coming from users should never be trusted. This includes $_GET, $_POST, $_REQUEST, $_COOKIE, $_FILES, $_SERVER, web services and external APIs.
Various – we’ll be talking specifics next
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

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

Magic Quotes

A PHP ini setting that tries to sanitize user input by prefixing all ', ", \ and null byte characters with a backslash.
It creates a false sense of security and produces input that is invalid for many tasks.
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

A request from a malicous site that uses a visitor’s authenticated state to perform unauthorized operations
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
Use the HTTP POST method for any actions other than viewing a page, require authentication tokens be sent as a request parameter


	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 ?>" />

Request Value Fixation

When the $_REQUEST superglobal is used and a cookie has the same name as a GET or POST parameter
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.
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

Letting a visitor write raw HTML that will be included in HTML output
Can be used to steal session cookies and run arbitrary scripts in browser
HTML Purifier (, 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 */

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

Visitor input that is concatenated or inserted into a string containing a SQL query
Attackers can manipulate database contents, escalate their authentication level
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",

Email Injection

When a visitor can input content that is placed into email headers, such as the From: header
Using fetures of the MIME email standard, malicious users can embed spam messages in the original email headers
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

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

When allowing file uploads, don’t trust user input
A user could upload a PHP file or other executable script. Mime types reported in the $_FILES superglobal can be faked.
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

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

Password Hashing

Any site the provides user login functionality that relies on passwords
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
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.


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

If the session identifier can be manipulated to provide a known, authenticated session to an attacker
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.
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

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.


When a session identifier is intercepted and used to gain access to a user’s account
The session ID can be intercepted in transit if transfered over a non-secure connection. Biggest vulnerability is on open wifi networks.
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

When the directory storing session files is shared between multiple sites
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.
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

When user input it used to read or manipulate the file system
Attackers can read files they shouldn’t have access to by using parent directory traversal ../ or alternate encodings, such as hex.
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.


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

When error messages are shown to visitors that include file system paths
Can be used with Path Traversal to access files, can be used to detect library/app versions and thus vulnerabilities
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

If any PHP (anywhere on the server) tries to seed the PRNG via srand() or mt_srand()
Predictable seeds allow for attackers to determine “random” values, such as password reset codes
Use Suhosin patch ( 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);
// Shared Windows host
$capi  = new COM('CAPICOM.Utilities.1');
$bytes = base64_decode($capi->getrandom(4, 0));
$seed = (int) (base_convert(bin2hex($bytes), 16, 10) - 2147483647);


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

Where Can I Learn More?