Difference between revisions of "PHP Security Cheat Sheet"

From OWASP
Jump to: navigation, search
(added PDO prepared statement wrapper)
(Database section complete, version 1)
Line 8: Line 8:
  
 
=Database Cheat Sheet=
 
=Database Cheat Sheet=
Since a single SQL Injection vulnerability makes for hacking of your website, and every hacker first tries SQL injection flaws, abide to the following rules:
+
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:
  
==Everything is a string for a database==
+
==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.  
 
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===
+
====Wrong====
 
  $x=1;  
 
  $x=1;  
 
  SELECT * FROM users WHERE ID > $x
 
  SELECT * FROM users WHERE ID > $x
===Right===
+
====Right====
 
  $x=1; // or $x='1';
 
  $x=1; // or $x='1';
 
  SELECT * FROM users WHERE ID >'$x';
 
  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==  
 
==Escaping is not safe==  
Line 25: Line 33:
  
 
'''Why:'''
 
'''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 you can't force yourself in any way. Also number fields might also be vulnerable if not used as strings. Instead use prepared statements or equivalent.
+
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==
 
==Use Prepared Statements==
Line 133: Line 141:
 
===Where prepared statements do not work===
 
===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:
 
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);
 +
 +
 +
 +
  
  
==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');
 
  
  

Revision as of 15:30, 2 July 2012

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

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.


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);






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

Andrew van der Stock