PHP security
Posted on 03 May 2009
A list of precautions I need to remember to include on every site/page. The key concept is – don’t trust any external data. $_POST variables can be set by anyone who creates their own form but puts the action as your page. $_GET can be set by them typing the URL in their browser. $_COOKIE variables can be set by editing the cookies on their system. $_SESSION variables can be accessed across sites on a shared server. Even http header data can be changed by using telnet amongst other methods. So check any of that data before you accept it for use. Filter it.
Check that register_globals is disabled
If enabled this setting allows people to access variables very easily. It’s turned off by default on almost all new installations but it’s worth checking. It is turned off and on at the server-level in a number of ways so check server documentation or ask your hosting company. Some use a file called .htaccess but my Ubuntu & PHP5 setup keeps it in /etc/php5/apache2/php.ini.
If this causes your php to fail then you need to stop referring to cookie, get and post variables in shorthand and use the now accepted methods:
$name=$_ POST[’name];
If you cannot turn register_globals off then make sure you initialise all your variables before using them (don’t just assume that they are null to start). This will over-write anything a user has managed to change and once the page is running they cannot -re-change the variables.
Check that values from forms are a suitable length and type
Although this is fairly basic why not check how long the values are? An email address of more than 50 characters is unlikely so if you get one maybe it is an attempt to inject malicious code through your form. All you need is and if statement using strlen() before the data is used. The same applies to names, dates and other likely fields. Of course there is always someone with a genuine email of more than 50 characters!
With numbers it’s easier. If you have asked for a date then check that it only has numerical data with separators and that the numbers are valid (not month 13 for example). This will also reduce problems caused by users who enter the date in the wrong way so you should be doing it anyway.
People re-writing your site
Don’t echo (or print, print_r etc.) any externally provided variables which have not been cleaned up first. Short of hacking your server it is possible for someone to insert Javascript into your page by putting it intoa field in your form (perhaps a log in form) which you then output to the browser.
If the data is never output to the browser then there are better ways to clean it (see SQL injection) for storage – do not use this method to assign data to variables for use in comparisons or storage in a database – just when the data is output to the browser.
To prevent unintended HTML being generated just strip out any potentially dangerous HTML tags (single quotes, double quotes, <, > and $) before echoing the variables to the browser:
$cleanusername=htmlspecialchars($_POST(’username’);
You would need to do this for $_GET and $_COOKIE as well because they can be manipulated by the user. You can also use htmlentities() or strip_tags(). The first does the same as htmlspecialchars but takes out every HTML special character (spaces etc.) while strip_tags just deletes the tags which is probably less useful.
A good habit to get into is to store all cleaned variables in an array (declare the array at the start of every script page:
hsc=Array();
then clean the variable just before using it to output:
hsc[’username’]=htmlspecialchars($username);
Then remember to never echo anything which is not in that array. This way you can use the original variable ($username) for all comparisons and database operations but the cleaned version to output. Just remember to update the cleaned version if the original has changed!
SQL injection
You will see many recommendations on this one so I have included most here so that you can see why only the last one is still recommended. First what happens…
Picture a form which asks for a username and password . These are extracted from $_POST and stored in variables used in this query:
SELECT FROM users WHERE username=’$username’ AND password=’$password’
picture how that query arrives at the SQL server if the password typed in is:
it will look like this:SELECT FROM users WHERE username=’fred’ AND password=’letmein’ OR ‘1′=’1′
which will always be true because 1 is 1 (I think) so they get in. The same sort of thing can be done with any form variable, $_GET and $_COOKIE all of which can be changed by the user to include SQL.
No longer recommended: To avoid it you once had to use addslashes() which escapes (using \ to say – the next character is not a command character but just a boring piece of text) all single quotes, double quotes, backslashes and NULL characters. So when you get the variables from $_POST:
$password=addslashes($_POST(’password’));
and store the password like that in the SQL database. When you want to use the variable in other ways:
stripslashes($password)
Unfortunately SQL injection can be done in any form, URL or cookie variable. This is obviously a lot of work for the coder so a shortcut was introduced (this also is no longer recommended). Recent php installations often automatically addslashes to all such variables (POST, GET and COOKIE data) depending on a setting in php.ini called magic_quotes_gpc. If that is turned on then all such data should be made safe for you. However, that function is being deprecated and should not be relied on (it is also slow). Instead you should use a function very similar to addslashes:
$user=mysql_real_escape_string($user);
This can only be done once there is a connection to the database so do the connect first then sort the strings out. This function is preferred as it escapes what needs to be escaped by SQL.
To summarise (contradicting what you may read elsewhere): addslashes is not recommended for SQL queries, magic_quotes should be turned off to save processing and mysql_real_escape_string() should be used on every GET, POST and COOKIE variable which is to be used in a query.
Session variables are not private
Session variables are stored in a directory on the server as text files. The default location is set in php.ini and on a shared server these session variables can be seen by any site because they are owned by the server user name (e.g. www-data) which is common to all sites on the server. You can override the default location and store your session variables somewhere else. If the server is set up well no other site should be able to access your session variables if they are held in a sub-directory of your site. Use session_save_path to change the location in your scripts each time you start a session.
You can also totally change the location in your script using session_set_save_handler() which will allow you to save session variables in a database rather than as a file.
You can also use md5() or sha1() to encrypt the variables (but they can still be copied which would not be ideal if the variable was a password). Alternatively you can use just one session variable (a user/session ID number) and store all the other variables in a database but then an attacker only needs to guess the session ID to access the rest of the data and be logged on to your site.
The session ID itself (which uniquely identifies the user) can be accessed if GET is used to pass the session ID rather than storing it as a cookie. By default PHP will try cookies and if the user has disabled them it will use GET to send the session ID in the URL. That URL travels in plain text and can be spotted by anyone monitoring network traffic and possibly also by an external site which you link to from one of your pages. In php.ini change the setting for session.use_only_cookies to 1 but you will need to warn users that they need to enable cookies to use your site.
Encrypt passwords in MYSQL
When inserting or updating passwords use the MySQL password() function to encrypt them. This means that if your database is compromised the passwords are not available in clear text:
INSERT INTO atable (username, password) VALUES ($username, password(‘$password’))
Then do the same with your select queries to get the password back. You can also use the functions md5() or SHA1() instead of password() which use different encryption methods and will be even more secure although password() is fairly good in later versions of php. Note that it is SHA1 with the digit 1 not a lower case l!
Of course the password is still travelling as clear text by http unless you have enabled secure connections and can be snooped (use SSL if this is a problem).
Encrypt your passwords in PHP
When you create a new password for a user you can use crypt($password) to encrypt the password before storing it. Then when the user types their password in for log on you can compare the encrypted password by using crypt() on the entered log in password as well. To be more secure you should use crypt($password, fred) where fred is a random string used in the encryption to make it harder for someone to encrypt with the same results. To encrypt or decrypt that one someone would have to guess your “salt” as the extra string is known. You can store the salt in the database for use when comparing passwords but obviously it makes sense to encrypt that as well!
You can even md5 or sha1 it in php and still use password() to re-encrypt it in MySQL.
Error messages
PHP with problems will display error messages in the browser. This is a great help while you develop but also gives information to a hacker. Turning off display_errors in php.ini is an easy solution once the site is publicly available (after you are finished with it). Meanwhile keep developing new improvements on a development server with error reporting on (perhaps something like XAMPP).
Protected areas of your site
If you have areas which require logging in then place a log in check on every page in that area. Write the login check as a separate page and then include it first thing on every other page. You can record in a database if a user has access to each page or you can store a session variable (e.g. $_SESSION[’admin’] set to 1 if authorised or 0 if not). Don’t use a cookie or $_GET or $POST as they can all be altered by the user.
Protecting your source code
PHP is best run as a module “plugged in” to the server. If that is the case then php pages will never be shown in the browser window. Instead they are processed by the server and just the results go to the browser. That’s probably how your server works but check. Many PHP pages include things like MySQL passwords.
Permissions on the php files and the directories they are in should normally allow execution and editing only by the server. For example, with a Linux/Apache set up (a LAMP server) the owner should be www-data and the permissions rwx—— (700) are ideal. Depending how you get the files there the owner might not be www-data so in Linux:
sudo chown -R www-data /server/www
but put your web site directory in instead of /server/www (the -R says do the same to any sub-directories). To change permissions I tend to use WinSCP because I use that for secure copying of the files from my Windows PC to the server but you could also type:
sudo chmod -R 700 /server/www
Also avoid having backup copies of your php files with any extension other than .php (some people keep old versions as index.bak for example). That file will then be shown (by some servers) as plain text in the browser revealing all your code.
If your site uses a lot of file access then consider running php in safe mode. This means files can only be accessed by their owner regardless of permissions (so any attempt by php under apache to access any file or directory not owned by www-data will fail). This is set in php.ini (safe_mode=on).
Include and require
include() allows you to bring in scripts or content from other files. But if you link it with user input it can include any file including remote ones. e.g. if you allow a user to type in a file name to include in a form or using a $_POST variable then they can type in a remote file name as easily as a local one:
include($page)
where $page is http:///www.evilempire.com/kill.php. If you have to use includes then use a switch statement to set all of the possible choices (so if the $page does not match any there is no included page).
The other warning about includes is to do with the files to be included. Don’t give them a .inc extension use .php. Otherwise the user can type their name as a URL and view the contents. .php files will always be processed not shown raw.
eval()
If you have never used it then don’t as it is slow and dangerous. If you have to then make sure you check the content of the string before you use it. Try this for more detail.
Execution of server files
Although your web server should run as a user with limited permissions (www-data for example) there is still the chance of someone running commands on the server if you use any of these in your scripts:
- system()
- exec()
- popen()
- passthru()
- the backtick (`)
The send commands to the server as if typed at a command prompt. The problem is that if users can input the command or part of it they can include characters which will achieve something else. For example if they include a semi-colon then anything after that is seen as a new command. So your intention might be for them to use:
system(’finger ‘.$username)
but if they type in “fred; reboot” as the user name your server reboots! There is a function to avoid this as it strips out those risky characters as htmlspecialchars does for html code:
escapeshellcommand(system(’finger ‘.$username))
If you do not expect to have to run any system commands from php then it is safest to disable the listed functions (system(), exec() etc.) in the disable_functions section of php.ini.
Uploading files
You might want users to be able to upload files. If so they could:
- upload lots of very big files to overload the server filling space and stopping other people accessing it (DoS)
- get you to pass on files with viruses or other worrying content to other users
- upload executable files and use your server to run them
In php.ini change two values:
upload_max_filesize=100K (or whatever maximum is appropriate)
post_max_size=150K (the total size of the form including the file)
After uploading a file use chmod in your script to make the file non-executable:
chmod($uploadedfile, 0644)
Cookies
These are stored in text files on the users machine. They can edit them. So they could insert any values into them. htmlspecialchars has dealt with that part of the problem but what if the cookie recorded something sensitive but valid (a record of actions or levels achieved in a game)? Encrypting the cookie makes it very hard for them to edit the values in it but not impossible (it is easy to return the cookie to a value it was one before – they can just copy the old encrypted value over the new one). So don’t use cookies for anything which the user might benefit from editing. Use session variables instead or store the values in a database.
Some validation examples and other possible checks
Switches
switch ($_POST[’charactertype’])
{
case ‘elf’:
case ‘orc’:
case ‘goblin’:
$race = $_POST[’charactertype’];
break;
}
If the value matches any of the three possibilities it is accepted (then add a default to tell the user they are doing it wrong).
Integers
if ($_POST[’number’] == strval(intval($_POST[’number’])))
{
$number = $_POST[’number’];
}
Email addresses
if (preg_match(/^[^@\s<&>]+@([-a-z0-9]+\.)+[a-z]{2,}$/i, $_POST[’email’]))
{
$email = $_POST[’email’];
}
Browser being used
Not very effective but it’s an extra check to make things hard for them. Record the browser being used any time they log in using:
$_SESSION[’HTTP_USER_AGENT’] = md5($_SERVER[’HTTP_USER_AGENT’]);
Then in your included file (the one which checks user rights for every protected page) check to see if it is still the same. If they switch browsers they will expect to log on again anyway. User agent switching is a problem but anyone using it will not be surprised if they then get strange results.
Some methods which don’t make sense
These are ones I have seen recommended but just don’t get.
Strip risky characters from form postings
If you use forms on your pages and then take that data for use in a database then someone could type in PHP code rather than what you had expected. Just because the form asks for an email address doesn’t mean the user will type that in. The risky characters include anything which is not a letter or number.
To strip them use this function:
preg_replace(’/[^a-z0-9]/i’, ”, @$_POST[’name’])
This looks at the variable ($_POST[’name’] and replaces anything which is not a standard letter or number with a null (deletes it). Obviously if you are trying to pass non-standard characters this would kill your script! Also imagine a username input in a form such as “mad-dog”. The – gets deleted and the username is not recognised. How does that help (please explain if I missed the point). Maybe there are a few situations where this would work but I doubt it.
Check for file types being uploaded
In your scripts use an if statement to check the file type by looking for a string in the file name:
if (preg_match(”.jpg”, $file) || preg_match(”.gif”, $file)) {
so only certain types of image will be allowed. But what is to stop a file called “thisisavirus.gif.exe” being uploaded?
No responses yet. You could be the first!