Will Bond
May 25th, 2011
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
Being aware of security vulnerabilities is paramount to writing secure code.
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.
Content-Type:
HTTP header or the HTML
<meta>
tag
Content-Type:
header and clean input values for invalid characters —
you should use UTF-8 for everything
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']);
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';
$_GET
,
$_POST
,
$_REQUEST
,
$_COOKIE
,
$_FILES
,
$_SERVER
,
web services and external APIs.
// 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);
$_GET
,
$_POST
,
$_REQUEST
,
$_COOKIE
,
$_FILES
and
$_SERVER
.
register_globals
ini setting to 0
– this is the default for PHP 5
'
,
"
, \
and null byte
characters with a backslash.
magic_quotes_gpc
and
magic_quotes_runtime
ini settings to 0
– this is the default for PHP 5
Also commonly known as CSRF or XSRF
POST
method for any actions other than viewing a page, require
authentication tokens be sent as a request parameter
<?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
superglobal is used and
a cookie has the same name as a GET
or
POST
parameter
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
.
$_GET
,
$_POST
or $_COOKIE
.
In PHP 5.3, the request_order
ini setting exists.
Also commonly known as XSS
htmlspecialchars()
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') ?>!
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;
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'] );
From:
header
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
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);
$_FILES
superglobal can be faked.
.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.
crypt()
with a Blowfish cipher salt,
or thousands of rounds of a random salt combined with the
sha1()
hash
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.
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
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.
session.use_only_cookies
ini
value to 1
. Regenerate session IDs
upon privilege escalation with
session_regenerate_id()
.
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.
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.
session_save_path()
.
Optionally encrypt a token in the session.
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.
../
or alternate encodings, such as hex.
realpath()
to get the canonical path
and compare with a whitelist
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 }
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.
srand()
or
mt_srand()
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.
// 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);