This site is the archived OWASP Foundation Wiki and is no longer accepting Account Requests.
To view the new OWASP Foundation website, please visit https://owasp.org
Difference between revisions of "OWASP Backend Security Project PHP Security Programming"
Line 509: | Line 509: | ||
=== Data Validation === | === Data Validation === | ||
+ | |||
+ | Modern WEB Applications are supposed to interacts with users throught input data. Input data can be supplied through a HTML Form and WEB Application retrieves such a data through a GET/POST variable. Input data can contains malicious values to exploit some | ||
+ | security flaws in WEB Applications. As a general rule data validation should be performed on both input and output values, since they both depends on each other. data should be rejected unless it matches a criteria. Developers should define a restriced range for valid data and reject everything else. Such a criteria will include: | ||
+ | * Data Type | ||
+ | * Data Length; | ||
+ | * Data Value | ||
+ | |||
+ | A typical Data Validation workflow will be: | ||
+ | * Get the data to be validated | ||
+ | * Check if it should be a numerical or string | ||
+ | * Look at it's size in byte to avoid errors when database table columns has some constraint in value size | ||
+ | * Check if data contains a valid value (EMail, phone number, date, and so on). | ||
+ | |||
+ | To this aims PHP can help developers with : | ||
+ | * casting operators | ||
+ | * regexp functions | ||
+ | |||
+ | |||
+ | |||
+ | '''Numeric Data''' | ||
+ | |||
+ | PHP is not a strongly typed languages it means that every input data is a string by default. If you want to validate a numeric value you should casting operator. Casting an input data to ''int'' ensure that: | ||
+ | |||
+ | * if data is numeric you get its value | ||
+ | * if data doesn't contains a number casting will returns 0 | ||
+ | * if data includes a number casting will returns its numeric portion | ||
+ | |||
+ | '''Example''' | ||
+ | |||
+ | <nowiki> | ||
+ | ... | ||
+ | $iId = (int)$_GET['id']; | ||
+ | if ( $iId != $_GET['id']) { | ||
+ | /* User supplied data is not numeric, handle exception */ | ||
+ | ... | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if ($iId > MAX_ID_VALUE || $iId < MIN_ID_VALUE) { | ||
+ | /* User supplied data is numerica but it doesn't contains an allowed value, handle exception */ | ||
+ | |||
+ | } | ||
+ | |||
+ | /* $iId is safe */ | ||
+ | |||
+ | |||
+ | </nowiki> | ||
+ | |||
+ | |||
+ | |||
+ | ''' String Data''' | ||
+ | |||
+ | Strings data validation is a bit tricker since it can contains malicious values. It means that it should be validated | ||
+ | on what data is supposed to include. Data can contains: | ||
+ | * EMail Address | ||
+ | * Phone Number | ||
+ | * URL | ||
+ | * Name | ||
+ | * Date | ||
+ | |||
+ | and so on. | ||
+ | |||
+ | To this aim WEB Developers should match Input Data against a Regular Expression to match what Data is supposed to inclues. | ||
+ | Here follows some example. | ||
+ | |||
+ | '''Example: Validating an Email Address''' | ||
+ | |||
+ | <nowiki> | ||
+ | ... | ||
+ | $sEmail = $_POST['email']; | ||
+ | if (! preg_match("/^[\w-]+(?:\.[\w-]+)*@(?:[\w-]+\.)+[a-zA-Z]{2,7}/", $sEmail)) { | ||
+ | /* User supplied data is not a valid email address, handle exception */ | ||
+ | ... | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | /* $sEmail is safe, check len */ | ||
+ | |||
+ | if (strlen($sEmail) > MAX_EMAIL_LEN) { | ||
+ | /* User supplied data is to big for backend database, handle exception */ | ||
+ | ... | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | |||
+ | |||
+ | </nowiki> | ||
+ | |||
+ | |||
+ | '''Example: Validating an Italian Phone Number ''' | ||
+ | <nowiki> | ||
+ | ... | ||
+ | $sPhoneNumber = $_POST['phonenumber']; | ||
+ | |||
+ | if (! preg_match(, "/[0-9]+[-\/ ]?[0-9]+/", $sPhoneNumber)) { | ||
+ | /* User supplied data is not a phone number, handle exception */ | ||
+ | ... | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | /* $sPhoneNumber is safe, check len */ | ||
+ | |||
+ | if (strlen($sPhoneNumber) > MAX_PHONENUMBER_LEN) { | ||
+ | /* User supplied data is to big for backend database, handle exception */ | ||
+ | ... | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | |||
+ | </nowiki> | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | ''' OWASP PHP Filters''' | ||
+ | |||
+ | OWASP PHP Filters project allow programmers an easy way to perform data validation. Even if project is quite old and not | ||
+ | well mantained it's still well working and defines a valid approach to performa Data Validation. | ||
=== Logging Errors === | === Logging Errors === | ||
+ | |||
+ | Malicious users typicaly attempt to explot SQL Injection Vulnerabilities by looking at some Error Codes on dynamic pages. | ||
+ | When PHP fails to query Backend Database an error message will be returned to users if error are not handled on a safe way. | ||
+ | WEB Developers uncorrectly debug SQL Errors by displaying some kind of error message on WEB Page when query fails. This approach should not be considered safe since Errors should never be displayed to users. | ||
+ | |||
+ | Users should also never deduce that something wrong happens otherwise it could be considered a flaw to further more exploits a vulnerability. To this aim we're going to how to safely handle errors. | ||
+ | |||
+ | <nowiki> | ||
+ | function unavailable_resource_handler() { | ||
+ | /* Handle an 'Unavailable Resource' event without supplying further information to user | ||
+ | * | ||
+ | * Example: | ||
+ | * die('Resource not available'); | ||
+ | * | ||
+ | * | ||
+ | */ | ||
+ | .. | ||
+ | .. | ||
+ | |||
+ | } | ||
+ | |||
+ | function sql_error_handler ($sQuery, $sMsg) { | ||
+ | /* Log failed SQL Query statement */ | ||
+ | error_log ($sQuery, 3, "/var/log/site/sqlquery_error.log"); | ||
+ | |||
+ | /* Log error message */ | ||
+ | error_log ($sMsg, 3, "/var/log/site/site_error.log"); | ||
+ | |||
+ | /* Notify user that resource is unavailable */ | ||
+ | unavailable_resource_handler(); | ||
+ | } | ||
+ | |||
+ | </nowiki> | ||
== PHP preventing LDAP Injection == | == PHP preventing LDAP Injection == | ||
Line 526: | Line 677: | ||
= References = | = References = | ||
− | |||
* OWASP : "OWASP Guide Project" - http://www.owasp.org/index.php/OWASP_Guide_Project | * OWASP : "OWASP Guide Project" - http://www.owasp.org/index.php/OWASP_Guide_Project | ||
+ | * OWASP : "OWASP PHP Filters" - http://www.owasp.org/index.php/OWASP_PHP_Filters | ||
+ | * OWASP : "OWASP PHP Project" - http://www.owasp.org/index.php/Category:OWASP_PHP_Project | ||
* PHP : "MySQL Improved Extension" - http://it2.php.net/manual/en/book.mysqli.php | * PHP : "MySQL Improved Extension" - http://it2.php.net/manual/en/book.mysqli.php | ||
+ | * Ilia Alshanetsky : "architect's Guide to PHP Security" - http://dev.mysql.com/tech-resources/articles/guide-to-php-security-ch3.pdf |
Revision as of 14:10, 20 July 2008
Overview
Example 1
Here follows a tipical Login Forms to authenticate user. Credentials are retrieved on a backend Database by using connection parameters stored in a .inc file.
auth.php <?php include('./db.inc'); function sAuthenticateUser($username, $password){ $authenticatedUserName=""; if ($link = iMysqlConnect()) { $query = "SELECT username FROM users"; $query .= " WHERE username = '".$username."'"; $query .= " AND password = md5('".$password."')"; $result = mysql_query($query); if ($result) { if ($row = mysql_fetch_row($result)) { $authenticatedUserName = $row[0]; } } } return $authenticatedUserName; } if ($sUserName = sAuthenticateUser($_POST["username"], $_POST["password"])) { /* successfull authentication code goes here */ ... ... } else { /* unsuccessfull authentication code goes here */ ... ... } ?>
db.inc <?php define('DB_HOST', "localhost"); define('DB_USERNAME', "user"); define('DB_PASSWORD', "password"); define('DB_DATABASE', "owasp"); function iMysqlConnect(){ $link = mysql_connect(DB_HOST, DB_USERNAME, DB_PASSWORD); if ($link && mysql_select_db(DB_DATABASE)) return $link; return FALSE; } ?>
The above example has two vulnerability:
- Authentication Bypass
- by exploiting a SQL Injection vulnerability Authentication you can authenticate as :
- username ' OR 1=1 #
- password anything
- by exploiting a SQL Injection vulnerability Authentication you can authenticate as :
- Information Disclosure
- an attacker may retrieve db.inc on unproper configured WEB Server
Example 2
The following sample code cames from a online book catalog.
getbook.php function aGetBookEntry($id) { $aBookEntry = NULL; $link = iMysqlConnect(); $query = "SELECT * FROM books WHERE id = $id"; $result = mysql_query($query); if ($result) { if ($row = mysql_fetch_array($result)) { $aBookEntry = $row; } } return $aBookEntry; } .... $id = $_GET['id']; $aBookEntry = aGetBookEntry($id); /* Display retrieved book information */ ... ...
The above example is vulnerable to Blind SQL Injection attack. An attacker exploiting this vulnerability may backup all Database, or interact with DBMS underlying Operating System.
Description
PHP preventing SQL Injection
When talking about preventing SQL Injection in PHP we shall take in consideration whatever to use old Database connector of newset PHP Portable Data Objects interface. To this aim will take in consideration both of them since:
- PHP PDO has been introduced in PHP 5.1
- PHP PDO represents a Database abstract layer
- Experienced developers may want to develop a custom Abstract Layer for theire demand
Reader should be aware that at the moment PDO OCI used as a Oracle Driver is experimental.
DBMS authentication credentials
When working with a DBMS through an authenticated connection application developers should be very carefull on how, and subsequently where, store authentication credentials to query such a backend engine. A configuration file with .inc extension should be avoided if left world wide readable by a web server since it's content can be easy retrieved. Such issue can be avoided by :
- Denying remote access to .inc files
<Files ~ “\.inc$”> Order allow,deny Deny from all </Files>
- requires user intervention on Apache configuration!
- Adding a security token
<?php if (defined('SECURITY_INCLUDE_TOKEN') && SECURITY_INCLUDE_TOKEN != 'WfY56#!5150'){ define ('DBMS_CONNECTION_STRING','mysql://owaspuser:owasppassword@localhost:3306'); .... }
<?php define('SECURITY_INCLUDE_TOKEN', 'WfY56#!5150'); include 'dbms_handler.php'; .. ?>
- configuring php_ini settings in apache
/etc/apache2/sites-enabled/000-owasp <VirtualHost *> DocumentRoot /var/www/apache2/ php_value mysql.default_host 127.0.0.1 php_value mysql.default_user owaspuser php_value mysql.default_password owasppassword .... .... .... </VirtualHost>
dbmshandler.php <?php function iMySQLConnect() { return mysql_connect(); } ?>
- at the moment of write it only works when backend Database Engine is MySQL
- using Apache SetEnv directive
/etc/apache2/sites-enabled/000-owasp <VirtualHost *> DocumentRoot /var/www/apache2/ SetEnv DBHOST "127.0.0.1" SetEnv DBUSER "owaspuser" SetEnv DBPASS "owasppassword" .... .... .... </VirtualHost>
dbmshandler.php <?php function iMySQLConnect() { return mysql_connect(getenv("DBHOST"), getenv("DBUSER"), getenv("DBPASS")); } ?>
- requires user intervention on Apache configuration
Wich one to use is up to you. You should take into account whetever a user interventation is needed on WEB Server or not and if you want to guarantee application portability as well between different DB Backend Engine.
Example using PDO MySQL driver:
/etc/apache2/sites-enabled/000-owasp <VirtualHost *> DocumentRoot /var/www/apache2/ SetEnv PDO_DSN "mysql:host=localhost;dbname=owasp" SetEnv PDO_USER "owaspuser" SetEnv PDO_PASS "owasppassword" .... .... .... </VirtualHost>
dbmshandler.php <?php function SQLConnect() { $oPdo = NULL; try { $oPdo = new PDO(getenv("PDO_DSN"), getenv("PDO_USER"), getenv("PDO_PASS")); /* Throws an exception when subsequent errors occour */ $oPdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); /* handle PDO connection success */ ... ... return $oPdo; } catch (PDOException $e) { /* handle PDO connection error */ ... ... return NULL; } } ?>
Escaping Quotes
magic_quotes_gpc escapes quotes from HTTP Request by examing both GET/POST data and Cookie value. The truth is that any other data in HTTP Request isn't escaped and an evil user may attempt to exploit a SQL Injection vulnerability on other HTTP Request data such as User-Agent value. Another drawback is that while it performs well on MySQL (as example) it doesn't works with Microsoft SQL Server where single quote should be escaped with ''' rather than \'
Since every application should be portable across WEB Servers we want to rollaback from magic_quotes_gpc each time a php script is running on WEB Server:
function magic_strip_slashes() { if (get_magic_quotes()) { // GET if (is_array($_GET)) { foreach ($_GET as $key => $value) { $_GET[$key] = stripslashes($value); } } // POST if (is_array($_POST)) { foreach ($_GET as $key => $value) { $_POST[$key] = stripslashes($value); } } // COOKIE if (is_array($_COOKIE)) { foreach ($_GET as $key => $value) { $_COOKIE[$key] = stripslashes($value); } } } }
and use a DBMS related function to escape quotes such as:
- MySQL: mysql_real_escape_string
- PostgreSQL: pg_escape_string
function sEscapeString($sDatabase, $sQuery) { $sResult=NULL; switch ($sDatabase) { case "mysql": $sResult = mysql_real_escape_string($sQuery); break; case "postgresql": $sResult = pg_escape_string($sQuery); break; case "mssql": $sResult = str_replace("'", "''",$sQuery); break; case "oracle": $sResult = str_replace("'", "''",$sQuery); break; } return $sResult; } }
Since both Oracle and Microsoft SQL Server connectors doesn't have a real escape_string function software developer can create his own escapeing functions or use addslasshes().
With properly quotes escaping we can prevent Authentication Bypass vulnerability in Example 1:
auth.php <?php include('./dbmshandler.php); function sAuthenticateUser($username, $password){ $authenticatedUserName=""; if ($link = iMysqlConnect()) { $query = "SELECT username FROM users"; $query .= " WHERE username = '".$username."'"; $query .= " AND password = md5('".$password."')"; /* escape quotes */ $result = sEscapeString("mysql", $query); if ($result) { if ($row = mysql_fetch_row($result)) { $authenticatedUserName = $row[0]; } } } return $authenticatedUserName; } /* start by rollback magic_quotes_gpc action (if any) */ magic_strip_slashes(); if ($sUserName = sAuthenticateUser($_POST["username"], $_POST["password"])) { /* successfull authentication code goes here */ ... ... } else { /* unsuccessfull authentication code goes here */ ... ... }
PHP Portable Data Objects implements a quote() method on PDO class but it's worst noticing that not all underlying PDO Drivers implements this method. On the other side PDO::query() method by defualt escape quotes on SQL query string as shown in following example.
Example using PDO MySQL driver:
auth.php <?php include('./dbmshandler.php'); function sAuthenticateUser($username, $password){ $authenticatedUserName=NULL; if ($oPdo = SQLConnect()) { $query = "SELECT username FROM users"; $query .= " WHERE username = '".$username."'"; $query .= " AND password = md5('".$password."')"; try { $row = $oPdo->query($query)->fetch(); if ($row) { return $row['username']; } } catch (PDOException e) { /* handle execption and SQL Injection Attempt */ .... .... return NULL; } } /* start by rollback magic_quotes_gpc action (if any) */ magic_strip_slashes(); if ($sUserName = sAuthenticateUser($_POST["username"], $_POST["password"])) { /* successfull authentication code goes here */ ... ... } else { /* unsuccessfull authentication code goes here */ ... ... }
Prepared Statements
Prepared Statements is the ability to preparse and generate an execution plan for SQL Queries. Such an execution plan will be instantiated with typed parameters. If params are of incorrect type or contains a nested query the execution of plan will fails.
<?php function getBookByID($id) { $aBook = NULL; $link = mysqli_connect(); $stmt = $link->stmt_init(); if ($stmt->prepare("SELECT * FROM books WHERE ID =?")) { $stmt->bind_param("i",$id); $stmt->execute(); /* Retrieves book entry and fill $aBook array */ ... ... /* Free prepared statement allocated resources */ $stmt->close(); } return $aBook; } /* MAIN */ /* Cast GET 'id' variable to integer */ $iID = (int)$_GET['id']; $aBookEntry = getBookByID($iID); if ($aBookEntry) { /* Display retrieved book entry */ ... ... } ?>
PHP Portable Data Objects emulate prepared statements for drivers with no natively supports. Here follows an example of prepared statements usage with PHP PDO
Example using PDO:
<?php include('./dbmshandler.php'); function getBookByID($id) { $aBook = NULL; $oPdo = SQLConnect(); if ($oPdo) { $stmt = $oPdo->prepare("SELECT * FROM books WHERE ID =?"); $stmt->bindParam(1, $id, PDO::PARAM_INT); if ($smmt->execute()) { $aBook = $stmt->fetch(PDO::FETCH_ASSOC); } } return $aBook; }
Data Validation
Modern WEB Applications are supposed to interacts with users throught input data. Input data can be supplied through a HTML Form and WEB Application retrieves such a data through a GET/POST variable. Input data can contains malicious values to exploit some security flaws in WEB Applications. As a general rule data validation should be performed on both input and output values, since they both depends on each other. data should be rejected unless it matches a criteria. Developers should define a restriced range for valid data and reject everything else. Such a criteria will include:
- Data Type
- Data Length;
- Data Value
A typical Data Validation workflow will be:
- Get the data to be validated
- Check if it should be a numerical or string
- Look at it's size in byte to avoid errors when database table columns has some constraint in value size
- Check if data contains a valid value (EMail, phone number, date, and so on).
To this aims PHP can help developers with :
- casting operators
- regexp functions
Numeric Data
PHP is not a strongly typed languages it means that every input data is a string by default. If you want to validate a numeric value you should casting operator. Casting an input data to int ensure that:
- if data is numeric you get its value
- if data doesn't contains a number casting will returns 0
- if data includes a number casting will returns its numeric portion
Example
... $iId = (int)$_GET['id']; if ( $iId != $_GET['id']) { /* User supplied data is not numeric, handle exception */ ... return; } if ($iId > MAX_ID_VALUE || $iId < MIN_ID_VALUE) { /* User supplied data is numerica but it doesn't contains an allowed value, handle exception */ } /* $iId is safe */
String Data
Strings data validation is a bit tricker since it can contains malicious values. It means that it should be validated on what data is supposed to include. Data can contains:
- EMail Address
- Phone Number
- URL
- Name
- Date
and so on.
To this aim WEB Developers should match Input Data against a Regular Expression to match what Data is supposed to inclues. Here follows some example.
Example: Validating an Email Address
... $sEmail = $_POST['email']; if (! preg_match("/^[\w-]+(?:\.[\w-]+)*@(?:[\w-]+\.)+[a-zA-Z]{2,7}/", $sEmail)) { /* User supplied data is not a valid email address, handle exception */ ... return; } /* $sEmail is safe, check len */ if (strlen($sEmail) > MAX_EMAIL_LEN) { /* User supplied data is to big for backend database, handle exception */ ... return; }
Example: Validating an Italian Phone Number
... $sPhoneNumber = $_POST['phonenumber']; if (! preg_match(, "/[0-9]+[-\/ ]?[0-9]+/", $sPhoneNumber)) { /* User supplied data is not a phone number, handle exception */ ... return; } /* $sPhoneNumber is safe, check len */ if (strlen($sPhoneNumber) > MAX_PHONENUMBER_LEN) { /* User supplied data is to big for backend database, handle exception */ ... return; }
OWASP PHP Filters
OWASP PHP Filters project allow programmers an easy way to perform data validation. Even if project is quite old and not well mantained it's still well working and defines a valid approach to performa Data Validation.
Logging Errors
Malicious users typicaly attempt to explot SQL Injection Vulnerabilities by looking at some Error Codes on dynamic pages. When PHP fails to query Backend Database an error message will be returned to users if error are not handled on a safe way. WEB Developers uncorrectly debug SQL Errors by displaying some kind of error message on WEB Page when query fails. This approach should not be considered safe since Errors should never be displayed to users.
Users should also never deduce that something wrong happens otherwise it could be considered a flaw to further more exploits a vulnerability. To this aim we're going to how to safely handle errors.
function unavailable_resource_handler() { /* Handle an 'Unavailable Resource' event without supplying further information to user * * Example: * die('Resource not available'); * * */ .. .. } function sql_error_handler ($sQuery, $sMsg) { /* Log failed SQL Query statement */ error_log ($sQuery, 3, "/var/log/site/sqlquery_error.log"); /* Log error message */ error_log ($sMsg, 3, "/var/log/site/site_error.log"); /* Notify user that resource is unavailable */ unavailable_resource_handler(); }
PHP preventing LDAP Injection
LDAP Authentication Credentials
Data Validation
Logging Errors
Detecting Intrusions from WEBAPP
Defeating Automated Tools
References
- OWASP : "OWASP Guide Project" - http://www.owasp.org/index.php/OWASP_Guide_Project
- OWASP : "OWASP PHP Filters" - http://www.owasp.org/index.php/OWASP_PHP_Filters
- OWASP : "OWASP PHP Project" - http://www.owasp.org/index.php/Category:OWASP_PHP_Project
- PHP : "MySQL Improved Extension" - http://it2.php.net/manual/en/book.mysqli.php
- Ilia Alshanetsky : "architect's Guide to PHP Security" - http://dev.mysql.com/tech-resources/articles/guide-to-php-security-ch3.pdf