PHP Security Cheat Sheet

From OWASP
Revision as of 20:07, 20 July 2012 by Abbas Naderi (Talk | contribs)

Jump to: navigation, search

Contents

Introduction

This page intends to provide quick basic PHP security tips for developers and administrators. Keep in mind that tips mentioned in this page are not enough for securing your web application.

this document is organized with regards to OWASP TOP 10. Solving more common issues helps reduce overal insecurity of

PHP status on the web

PHP is the most commonly used server-side programming language and 72% of web servers deploy PHP. PHP is totally open source and almost no commercial giant supports it. PHP core is considerably secure, but it's plugins, libraries and third party tools are usually unsafe. Also no default security mechanism is included in PHP (there were some in the old days, but they ruined things usually).

PHP developers are usually more informative than ASPX or JSP developers on how web and HTTP works, and that makes for better coding practices, but they both lack basic security knowledge. Other languages have built-in security mechanisms, that's why PHP websites are more flawed these days.

Update PHP Now

Important Note: PHP 5.2.x is officially unsupported now. This means that in the near future, when a common security flaw on PHP 5.2.x is discovered, every PHP 5.2.x powered website is bound to be hacked. It is of utmost important that you upgrade your PHP to 5.3.x or 5.4.x right now.

Also keep in mind that you should regularly upgrade your PHP distribution on an operational server. Every day new flaws are discovered and announced in PHP and attackers use these new flaws on random servers frequently.

Database Cheat Sheet

Since a single SQL Injection vulnerability makes for hacking of your website, and every hacker first tries SQL injection flaws, fixing SQL injections are first step to securing your PHP powered application. Abide to the following rules:

Encoding Issues

Everything is a string for a database

There are ways to send different data types to a database, ints, floats, etc. Never rely on them, instead always send an string to the database. Database engines type cast automatically if they need to. This makes for much safer queries. Make this a habit of yours, and see how many time it saves you.

Wrong

$x=1; 
SELECT * FROM users WHERE ID > $x

Right

$x=1; // or $x='1';
SELECT * FROM users WHERE ID >'$x';

Use UTF-8 unless necessary

Many new attack vectors rely on encoding bypassing. Use UTF-8 as your database and application charset unless you have a mandatory requirement to use another encoding.

   $DB = new mysqli($Host, $Username, $Password, $DatabaseName);
   if (mysqli_connect_errno())
       trigger_error("Unable to connect to MySQLi database.");
   $DB->set_charset('UTF-8');

Escaping is not safe

mysql_real_escape_string is not safe. Don't rely on it for your SQL injection prevention.

Why: When you use mysql_real_escape_string on every variable and then concat it to your query, you are bound to forget that at least once, and one is all it takes. You can't force yourself in any way to never forget. Number fields might also be vulnerable if not used as strings. Instead use prepared statements or equivalent.

Use Prepared Statements

Prepared statements are very secure. In a prepared statement, data is separated from the SQL command, so that everything user inputs is considered data and put into the table the way it was.


MySQLi Prepared Statements Wrapper

The following function, performs a SQL query, returns its results as a 2D array (if query was SELECT) and does all that with prepared statements using MySQLi fast MySQL interface:

   $DB = new mysqli($Host, $Username, $Password, $DatabaseName);
   if (mysqli_connect_errno())
       trigger_error("Unable to connect to MySQLi database.");
   $DB->set_charset('UTF-8');
   function SQL($Query) {
       global $DB;
       $args = func_get_args();
       if (count($args) == 1) {
           $result = $DB->query($Query);
           if ($result->num_rows) {
               $out = array();
               while (null != ($r = $result->fetch_array(MYSQLI_ASSOC)))
                   $out [] = $r;
               return $out;
           }
           return null;
       } else {
           if (!$stmt = $DB->prepare($Query))
               trigger_error("Unable to prepare statement: {$Query}, reason: " . $DB->error . "");
           array_shift($args); //remove $Query from args
           //the following three lines are the only way to copy an array values in PHP
           $a = array();
           foreach ($args as $k => &$v)
               $a[$k] = &$v;
           $types = str_repeat("s", count($args)); //all params are strings, works well on MySQL and SQLite
           array_unshift($a, $types);
           call_user_func_array(array($stmt, 'bind_param'), $a);
           $stmt->execute();
           //fetching all results in a 2D array
           $metadata = $stmt->result_metadata();
           $out = array();
           $fields = array();
           if (!$metadata)
               return null;
           $length = 0;
           while (null != ($field = mysqli_fetch_field($metadata))) {
               $fields [] = &$out [$field->name];
               $length+=$field->length;
           }
           call_user_func_array(array(
               $stmt, "bind_result"
                   ), $fields);
           $output = array();
           $count = 0;
           while ($stmt->fetch()) {
               foreach ($out as $k => $v)
                   $output [$count] [$k] = $v;
               $count++;
           }
           $stmt->free_result();
           return ($count == 0) ? null : $output;
       }
   }

Now you could do your every query like the example below:

$res=SQL("SELECT * FROM users WHERE ID>? ORDER BY ? ASC LIMIT ?" , 5 , "Username" , 2);

Every instance of ? is bound with an argument of the list, not replaced with it. MySQL 5.5+ supports ? as ORDER BY and LIMIT clause specifiers. If you're using a database that doesn't support them, see next section.

REMEMBER: When you use this approach, you should NEVER concat strings for a SQL query.

PDO Prepared Statement Wrapper

The following function, does the same thing as the above function but using PDO. You can use it with every PDO supported driver.

   try {
       $DB = new PDO("{$Driver}:dbname={$DatabaseName};host={$Host};", $Username, $Password);
   } catch (Exception $e) {
       trigger_error("PDO connection error: " . $e->getMessage());
   }
   function SQL($Query) {
       global $DB;
       $args = func_get_args();
       if (count($args) == 1) {
           $result = $DB->query($Query);
           if ($result->rowCount()) {
               return $result->fetchAll(PDO::FETCH_ASSOC);
           }
           return null;
       } else {
           if (!$stmt = $DB->prepare($Query)) {
               $Error = $DB->errorInfo();
               trigger_error("Unable to prepare statement: {$Query}, reason: {$Error[2]}");
           }
           array_shift($args); //remove $Query from args
           $i = 0;
           foreach ($args as &$v)
               $stmt->bindValue(++$i, $v);
           $stmt->execute();
           return $stmt->fetchAll(PDO::FETCH_ASSOC);
       }
   }
$res=SQL("SELECT * FROM users WHERE ID>? ORDER BY ? ASC LIMIT 5" , 5 , "Username" );

Where prepared statements do not work

The problem is, when you need to build dynamic queries, or need to set variables not supported as a prepared variable, or your database engine does not support prepared statements. For example, PDO MySQL does not support ? as LIMIT specifier. In these cases, you need to do two things:

Not Supported Fields

When some field does not support binding (like LIMIT clause in PDO), you need to whitelist the data you're about to use. LIMIT always requires an integer, so cast the variable to an integer. ORDER BY needs a field name, so whitelist it with field names:

   function whitelist($Needle,$Haystack)
   {
       if (!in_array($Needle,$Haystack))
               return reset($Haystack); //first element
       return $Needle;
   }
   $Limit = $_GET['lim'];
   $Limit = $Limit * 1; //type cast, integers are safe
   $Order = $_GET['sort'];
   $Order=whitelist($Order,Array("ID","Username","Password"));

This is very important. If you think you're tired and you rather blacklist than whitelist, you're bound to fail.

Dynamic Queries

Now this is a highly delicate situation. Whenever hackers fail to injection SQL in your common application scenarios, they go for Advanced Search features or similars, because those features rely on dynamic queries and dynamic queries are almost always insecurely implemented.

When you're building a dynamic query, the only way is whitelisting. Whitelist every field name, every boolean operator (it should be OR or AND, nothing else) and after building your query, use prepared statements:

   $Query="SELECT * FROM table WHERE ";
   foreach ($_GET['fields'] as $g)
       $Query.=whitelist($g,Array("list","of","possible","fields","here"))."=?";
   $Values=$_GET['values'];
   array_unshift($Query); //add to the beginning
   $res=call_user_func_array(SQL, $Values);


ORM

ORMs are good security practice. If you're using an ORM (like Doctrine) in your PHP project, you're mostly prone to SQL attacks. Although injecting queries in ORM's is much harder, keep in mind that concatenating ORM queries makes for the same flaws that concatenating SQL queries, so NEVER concatenate strings sent to a database. ORM's support prepared statements as well.



Other Injection Cheat Sheet

SQL aside, there are a few more injections possible and common in PHP:

Shell Injection

A few PHP functions namely

run a string as shell scripts and commands. Input provided to these functions (specially backtick operator that is not like a function). Depending on your configuration, shell script injection can cause your application settings and configuration to leak, or your whole server to be hijacked. This is a very dangerous injection and is somehow considered the haven of an attacker.

Never pass tainted input to these functions - that is input somehow manipulated by the user - unless you're absolutely sure there's no way for it to be dangerous (which you never are without whitelisting). Escaping and any other countermeasures are ineffective, there are plenty of vectors for bypassing each and every one of them; don't believe what novice developers tell you.


Code Injection

All interpreted languages such as PHP, have some function that accepts a string and runs that in that language. It is usually named Eval. PHP also has Eval. Using Eval is a very bad practice, not just for security. If you're absolutely sure you have no other way but eval, use it without any tainted input.

Reflection also could have code injection flaws. Refer to the appropriate reflection documentations, since it is an advanced topic.

Other Injections

LDAP, XPath and any other third party application that runs a string, is vulnerable to injection. Always keep in mind that some strings are not data, but commands and thus should be secure before passing to third party libraries.


XSS Cheat Sheet

There are two scenarios when it comes to XSS, each one to be mitigated accordingly:

No Tags

Most of the time, output needs no HTML tags. For example when you're about to dump a textbox value, or output user data in a cell. In this scenarios, you can mitigate XSS by simply using the function below. Keep in mind that this scenario won't mitigate XSS when you use user input in dangerous elements (style, script, image's src, a, etc.), but mostly you don't. Also keep in mind that every output that is not intended to contain HTML tags should be sent to the browser filtered with the following function.

//xss mitigation functions
function xssafe($data,$encoding='UTF-8')
{
	return htmlspecialchars($data,ENT_QUOTES | ENT_HTML401,$encoding);
}
function xecho($data)
{
	echo xssafe($data);
}
//usage example
<input type='text' name='test' value='<?php 
xecho ("' onclick='alert(1)");
?>' />

Yes Tags

When you need tags in your output, such as rich blog comments, forum posts, blog posts and etc., you have to use a Secure Encoding library. This is usually hard and slow, and that's why most applications have XSS vulnerabilities in them. OWASP ESAPI has a bunch of codecs for encoding different sections of data. There's also OWASP AntiSammy and HTMLPurifier for PHP. Each of these require lots of configuration and learning to perform well, but you need them when you want that good of an application.

Other Tips

  • We don't have a trusted section in any web application. Many developers tend to leave admin areas out of XSS mitigation, but most intruders are interested in admin cookies and XSS. Every output should be cleared by the functions provided above, if it has a variable in it. Remove every instance of echo, print, and printf from your application and replace them with the above statement when you see a variable is included, no harm comes with that.
  • HTTP-Only cookies are a very good practice, for a near future when every browser is compatible. Start using them now. (See PHP.ini configuration for best practice)
  • The function declared above, only works for valid HTML syntax. If you put your Element Attributes without quotation, you're doomed. Go for valid HTML.
  • Reflected XSS is as dangerous as normal XSS, and usually comes at the most dusty corners of an application. Seek it and mitigate it.


CSRF Cheat Sheet

CSRF mitigation is easy in theory, but hard to implement correctly. First, a few tips about CSRF:

  • Every request that does something noteworthy, should be CSRF mitigated. Noteworthy things are changes to the system, and reads that take a long time.
  • CSRF mostly happens on GET, but is easy to happen on POST. Don't ever think that post is secure.

The OWASP PHP CSRFGuard is a code snippet that shows how to mitigate CSRF. Only copy pasting it is not enough. In the near future, a copy-pasteable version would be available (hopefully). For now, mix that with the following tips:

  • Use re-authentication for critical operations (change password, recovery email, etc.)
  • If you're not sure whether your operation is CSRF proof, consider adding CAPTCHAs (however CAPTCHAs are inconvenience for users)
  • If you're performing operations based on other parts of a request (neither GET nor POST) e.g Cookies or HTTP Headers, you might need to add CSRF tokens there as well.
  • AJAX powered forms need to re-create their CSRF tokens. Use the function provided above (in code snippet) for that and never rely on Javascript.
  • CSRF on GET or Cookies will lead to inconvenience, consider your design and architecture for best practices.

Authentication and Session Management Cheat Sheet

PHP doesn't ship with a readily available authentication module, you need to implement your own or use a PHP framework, unfortunately most PHP frameworks are far from perfect in this manner, due to the fact that they are developed by open source developer community rather than security experts. A few instructive and useful tips are listed below:

Session Management

PHP's default session facilites are considered safe, the generated PHPSessionID is random enough, but the storage is not necessarily safe:

  • Session files are stored in temp (/tmp) folder and are world writable unless suPHP installed, so any LFI or other leak might end-up manipulating them.
  • Sessions are stored in files in default configuration, which is terribly slow for highly visited websites. You can store them on a memory folder (if UNIX).
  • You can implement your own session mechanism, without ever relying on PHP for it. If you did that, store session data in a database. You could use all, some or none of the PHP functionality for session handling if you go with that.

Session Hijacking Prevention

It is good practice to bind sessions to IP addresses, that would prevent most session hijacking scenarios (but not all), however some users might use anonymity tools (such as TOR) and they would have problems with your service.

To implement this, simply store the client IP in the session first time it is created, and enforce it to be the same afterwards. The code snippet below returns client IP address:

$IP = (getenv ( "HTTP_X_FORWARDED_FOR" )) ? getenv ( "HTTP_X_FORWARDED_FOR" ) : getenv ( "REMOTE_ADDR" );

Keep in mind that in local environments, a valid IP is not returned, and usually the string :::1 or :::127 might pop up, thus adapt your IP checking logic.

Invalidate Session ID

You should invalidate (unset cookie, unset session storage, remove traces) of a session whenever a violation occurs (e.g 2 IP addresses are observed). A log event would prove useful. Many applications also notify the logged in user (e.g GMail).

Rolling of Session ID

You should roll session ID whenever elevation occurs, e.g when a user logs in, the session ID of the session should be changed, since it's importance is changed.

Exposed Session ID

Session IDs are considered confidential, your application should not expose them anywhere (specially when bound to a logged in user). Try not to use URLs as session ID medium.

Transfer session ID over TLS whenever session holds confidential information, otherwise a passive attacker would be able to perform session hijacking.

Session Fixation

Session IDs are to be generated by your application only. Never create a session only because you receive the session ID from the client, the only source of creating a session should be a secure random generator.

Session Expiration

A session should expire after a certain amount of inactivity, and after a certain time of activity as well. The expiration process means invalidating and removing a session, and creating a new one when another request is met.

Also keep the log out button close, and unset all traces of the session on log out.

Inactivity Timeout

Expire a session if current request is X seconds later than the last request. For this you should update session data with time of the request each time a request is made. The common practice time is 30 minutes, but highly depends on application criteria.

This expiration helps when a user is logged in on a publicly accessible machine, but forgets to log out. It also helps with session hijacking.

General Timeout=

Expire a session if current session has been active for a certain amount of time, even if active. This helps keeping track of things. The amount differs but something between a day and a week is usually good. To implement this you need to store start time of a session.


HTTP Only

Most modern browsers support HTTP-only cookies. These cookies are only accessible via HTTP(s) requests and not Javascript, so XSS snippets can not access them. They are very good practice, but are not satisfactory since there are many flaws discovered in major browsers that lead to exposure of HTTP only cookies to javascript.

To use HTTP-only cookies in PHP (5.2+), you should perform session cookie setting manually (not using session_start):

#prototype
bool setcookie ( string $name [, string $value [, int $expire = 0 [, string $path [, string $domain [, bool $secure = false [, bool $httponly = false ]]]]]] )
#usage
if (!setcookie("MySessionID", $secureRandomSessionID, $generalTimeout, $applicationRootURLwithoutHost, NULL, NULL,true))
    echo ("could not set HTTP-only cookie");

The path parameter sets the path which cookie is valid for, e.g if you have your website at example.com/some/folder the path should be /some/folder or other applications residing at example.com could also see your cookie. If you're on a whole domain, don't mind it. Domain parameter enforces the domain, if you're accessible on multiple domains or IPs ignore this, otherwise set it accordingly. If secure parameter is set, cookie can only be transmitted over HTTPS. See the example below:

$r=setcookie("SECSESSID","1203j01j0s1209jw0s21jxd01h029y779g724jahsa9opk123973",60*60*24*7 /*a week*/,"/","owasp.org",true,true);
if (!$r) die("Could not set session cookie.");

Authentication

Remember Me

Many websites are vulnerable on remember me features. The correct practice is to generate a one-time token for a user and store it in the cookie. The token should also reside in data store of the application to be validated and assigned to user. This token should have no relevance to username and/or password of the user, a secure long-enough random number is a good practice.

It is better if you imply locking and prevent brute-force on remember me tokens, and make them long enough, otherwise an attacker could brute-force remember me tokens until he gets access to a logged in user without credentials.

  • Never store username/password or any relevant information in the cookie.

Access Control Cheat Sheet

This section aims to mitigate access control issues, as well as Insecure Direct Object Reference issues.

Cryptography Cheat Sheet

File Inclusion Cheat Sheet

Configuration and Deployment Cheat Sheet

Database User

suPHP

php.ini

Template:TBD: explain pros&cons what to set in php.ini and/or httpd.conf and/or registry

Note that some of following settings need to be adapted to your system. Also read the PHP Manual according dependencies of some settings.

PHP error handlling

 expose_php              = Off
 error_reporting         = E_ALL
 display_errors          = Off
 display_startup_errors  = Off
 log_errors              = On
 error_log               = /path/PHP-logs/php_error.log
 ignore_repeated_errors  = Off

PHP general settings

 doc_root                = /path/DocumentRoot/PHP-scripts/
 open_basedir            = /path/DocumentRoot/PHP-scripts/
 include_path            = /path/PHP-pear/
 extension_dir           = /path/PHP-extensions/
 mime_magic.magicfile 	  = /path/PHP-magic.mime
 allow_url_fopen         = Off
 allow_url_include       = Off
 variables_order         = "GPSE"
 allow_webdav_methods    = Off

PHP file upload handling

 file_uploads            = Off
 upload_tmp_dir          = /path/PHP-uploads/
 upload_max_filesize     = 1M   # NOTE: more or less useless as first handled by the web server
 max_file_uploads        = 2

PHP executable handling

 enable_dl               = On
 disable_functions       = system, exec, shell_exec, passthru, phpinfo, show_source, popen, proc_open
 disable_functions       = fopen_with_path, dbmopen, dbase_open, putenv, move_uploaded_file
 disable_functions       = chdir, mkdir, rmdir, chmod, rename
 disable_functions       = filepro, filepro_rowcount, filepro_retrieve, posix_mkfifo
   # see also: http://de3.php.net/features.safe-mode
 disable_classes         = 

PHP session handling

 session.auto_start      = Off
 session.save_path       = /path/PHP-session/
 session.name            = myPHPSESSID
 session.hash_function   = 1
 session.hash_bits_per_character = 6
 session.use_trans_sid   = 0
 session.cookie_domain   = full.qualified.domain.name
 session.cookie_path     = /application/path/
 session.cookie_lifetime = 0
 session.cookie_secure   = On
 session.cookie_httponly = 1
 session.use_only_cookies= 1
 session.cache_expire    = 30
 default_socket_timeout  = 60

some more security paranoid checks

 session.referer_check   = /application/path
 memory_limit            = 2M
 post_max_size           = 2M
 mx_execution_time       = 9
 report_memleaks         = On
 track_errors            = Off
 html_errors             = Off

old, depricated

Use these configurations in older PHP versions if necessary.

 register_globals        = Off
 gpc_order               = "GP"
 magic_quotes_gpc        = On
 safe_mode               = On
 safe_mode_include_dir   = /path/PHP-include
 safe_mode_exec_dir      = /path/PHP-executable
 safe_mode_allowed_env_vars   = PHP_
 safe_mode_protected_env_vars = SHELL, IFS, PATH, HOME, USER, TZ, TMP, TMPDIR, LANG, LD_LIBRARY_PATH, LD_PRELOAD, SHLIB_PATH, LIBPATH

Database Settings

Template:TBD: database sesttings should be done in web server's configuration (i.e. httpd.conf)

Sources of Taint

PHP General Guidelines for Secure Web Applications

PHP Version

Use PHP 5.3.8. Stable versions are always safer then the beta ones.

Framework

Use a framework like Zend or Symfony. Try not to re-write the code again and again. Also avoid dead codes.

Directory

Code with most of your code outside of the webroot. This is automatic for Symfony and Zend. Stick to these frameworks.

Hashing Extension

Not every PHP installation has a working mhash extension, so if you need to do hashing, check it before using it. Otherwise you can't do SHA-256

Cryptographic Extension

Not every PHP installation has a working mcrypt extension, and without it you can't do AES. Do check if you need it.

Authentication and Authorization

There is no authentication or authorization classes in native PHP. Use ZF or Symfony instead.

Input nput validation

Use $_dirty['foo'] = $_GET['foo'] and then $foo = validate_foo($dirty['foo']);

Use PDO or ORM

Use PDO with prepared statements or an ORM like Doctrine

Use PHP Unit and Jenkins

When developing PHP code, make sure you develop with PHP Unit and Jenkins - see http://qualityassuranceinphpprojects.com/pages/tools.html for more details.

Use Stefan Esser's Hardened PHP Patch

Consider using Stefan Esser's Hardened PHP patch - http://www.hardened-php.net/suhosin/index.html (not maintained now, but the concepts are very powerful)

Avoid Global Variables

In terms of secure coding with PHP, do not use globals unless absolutely necessary Check your php.ini to ensure register_globals is off Do not run at all with this setting enabled It's extremely dangerous (register_globals has been disabled since 5.0 / 2006, but .... most PHP 4 code needs it, so many hosters have it turned on)

Avoid Eval()

It basically allows arbitrary PHP code execution, so do not evaluate user supplied input. and if you're not doing that, you can just use PHP directly. eval() is at least 10-100 times slower than native PHP

Don't use $_REQUEST

Instead of $_REQUEST- use $_GET or $_POST or $_SERVER

Protection against RFI

Ensure allow_url_fopen and allow_url_include are both disabled to protect against RFI But don't cause issues by using the pattern include $user_supplied_data or require "base" + $user_supplied_data - it's just unsafe as you can input /etc/passwd and PHP will try to include it

Regexes (!)

Watch for executable regexes (!)

Session Rotation

Session rotation is very easy - just after authentication, plonk in session_regenerate_id() and you're done.

Be aware of PHP filters

PHP filters can be tricky and complex. Be extra-conscious when using them.

Logging

Set display_errors to 0, and set up logging to go to a file you control, or at least syslog. This is the most commonly neglected area of PHP configuration

Output encoding

Output encoding is entirely up to you. Just do it, ESAPI for PHP is ready for this job.

These are transparent to you and you need to know about them. php://input: takes input from the console gzip: takes compressed input and might bypass input validation http://au2.php.net/manual/en/filters.php

Related Cheat Sheets

OWASP Cheat Sheets Project Homepage

Developer Cheat Sheets (Builder)

Assessment Cheat Sheets (Breaker)

Mobile Cheat Sheets

OpSec Cheat Sheets (Defender)

Draft Cheat Sheets

Authors and Primary Editors

Abbas Naderi Afooshteh - abbas.naderi@owasp.org (owasp@abiusx.com)

Achim - achim@owasp.org

Legacy Author: Andrew van der Stock

--Abbas Naderi 00:34, 10 July 2012 (UTC)

Other Cheatsheets

OWASP Cheat Sheets Project Homepage

Developer Cheat Sheets (Builder)

Assessment Cheat Sheets (Breaker)

Mobile Cheat Sheets

OpSec Cheat Sheets (Defender)

Draft Cheat Sheets