16 Tips for Secure Code
Revision as of 23:57, 31 July 2014 by Viper-7
- Turn off register_globals! - This ancient php feature will let people create any variable they like in the global scope. Think about what happens when they set $admin = 1 or such. Many large sites and scripts have had major security due to this feature, and it's being removed from PHP in the near future. http://php.net/manual/en/security.globals.php
- Turn off magic_quotes! - Another ancient php feature which runs addslashes() on all user input to try and be 'helpful'. In reality you almost always want to do some processing or validation on data before the data is escaped, as it's much harder to validate things like length when you have extra \'s all through your input that don't actually count. Instead, if you wait until when you are ready to use the data in a query to escape it, you'll have an active database connection handy, and can use your database's escaping function to make sure you correctly escape for the connection character set. http://php.net/magic_quotes
- Never store any sensitive data in a cookie. Cookies can be very easily stolen, faked, etc. The only data you should usually need to put in a cookie is a randomly generated token, which you also store in your database, so you can use it to identify a specific user. This is how PHP sessions work, and you should simply use those wherever possible. (see below) http://php.net/sessions
- Don't extend the PHP session lifetime beyond an hour. If you do need to provide a "Remember Me" function: Generate a unique token, store it in that user's record in your database, and put that token in a cookie. If a user that doesn't have an active session requests a page, check for this token and use it to create a new logged in session for that user. It's a good idea to regenerate this token every time it's used to again ease security issues (unless clients need the ability to stay logged in on multiple computers.)
- If you're on a shared server, Use session_regenerate_id() after session_start() to generate a new session token on every request, rendering any token that may have been stolen or replaced useless. The 'session hijacking' attack surface still exists (if someone can find and replace the session data file between 2 requests from the client, before the 2nd request can regenerate the id), but it's greatly reduced, enough so to make it a non issue in my opinion. (it's about the best you'll ever hope for on a shared host, move to a vps or dedicated host if you want security.) http://php.net/session_regenerate_id
- Never trust the file extension or MIME type of a file in the $_FILES array when processing uploads. Both of these values are supplied by the client, and can be easily faked. Use http://php.net/finfo_file or http://php.net/getimagesize to fetch the MIME type based on the file's actual content. A well built implementation will rewrite the file extension based on the detected MIME type.
- Always escape all data that come from a user that are headed into your database (use mysql_real_escape_string() or bind the values to prepared statements using MySQLi or PDO). This is to prevent data supplied by the user, being confused with your SQL code. http://php.net/mysql_real_escape_string http://php.net/pdo
- Be sure to use escaping *correctly*! If you use mysql_real_escape_string() on a value from $_GET, then because you think its a number, you don't put quotes around the variable in your SQL - You're vulnerable to injection! Since an attacker doesn't need to "break out" of your quotes (because he's not inside any), he can inject raw SQL commands and mysql_real_escape_string() can do nothing to stop him. Now, Database engines don't particularly like it when you quote integers, as it causes them to run a bunch of internal code for each row to convert them, which is why people commonly try to leave the quotes off. You have three choices to solve this problem.
- Use ctype_digit() to validate that values in $_GET or such are infact numeric, and reject them if they are not.
- Use intval() or (int)$var casting to convert the value to an integer, this means if they try to submit a string, they'll probably get a value of 0.
- Always quote values in your queries, even integers. I find this rule much simpler for those still learning the ropes, as you don't "sometimes escape, sometimes convert, sometimes quote, sometimes not", you just always escape, always quote, and thats the end of it. Prepared statements also offer the same ease of use, and a great idea for new projects (available with MySQLi and PDO). The performance impact on your db for quoting an integer won't be noticible until you have a large number of rows per table (200,000+), but users of databases with tables much larger than that might not want to use this method.
- Always encode any data thats ever come from a user, for output in html, by using htmlentities() or htmlspecialchars() - WITH the appropriate character set supplied for your document (ie UTF-8). This is to prevent data submitted by a user from being confused as HTML code. If you're putting content into the attribute of a HTML tag, be sure to use the ENT_QUOTES option too. http://php.net/htmlentities http://php.net/htmlspecialchars
- Always make both your page character set (which is set by your Content-Type header, and/or by a <meta> tag in the document) and your database connection character set (which is set by mysql_set_charset() or a SET NAMES query under mysqli or PDO) match each other. You probably want to set both of these to UTF-8. (be aware that you may also need to select a unicode compatible collation in your database to have true unicode support)
- Always check that variables and files exist before using them. isset(), empty() and is_readable() are very helpful in writing code that can handle common issues cleanly, rather than exposing nasty php notices/warnings and possibly revealing some of your system's structure. By doing this, you can leave your error reporting settings at a high value, and get a high detail of logging so you can know what went wrong and why when a real problem does occur. You can still hide these errors from being displayed to your users by setting display_errors = 0.
- Always set error reporting settings BEFORE php starts up. All error reporting settings should be set in your php.ini or webserver config, or at worst in .htaccess. If you wait until PHP has started up and executes your code to run an error_reporting() function there, your server still may encounter errors before this (like when it compiles your script), and php will either show the errors to your end users (ugly) or hide them from you when debugging issues (makes things very difficult)
- Always use .php file extensions for php files. There are many other 'standards' people like to use like .inc, but without careful configuration of your webserver to block access to these files, using a non standard extension may expose huge amounts of your codebase to the public. I've seen this happen many times to people when they move to a new server, with new staff, and totally overlook the fact that the whole world can view their code, and Google is even indexing it! Naming conventions are a very good thing, either at the start or end, module_blog.php or blog.module.php are perfectly fine, just leave the .php at the end.
- Use basename() where possible on submitted file / folder names, to strip any path components like /../ people may try to inject. This only works if you're expecting a single name (like a page name), not an entire path. http://php.net/basename
- Try to avoid using shell functions like exec() and system() where you can (usually theres a php extension you can use instead that'll be both faster and more secure.) If you do need to run shell commands, be sure to escape all arguments using escapeshellarg() http://php.net/escapeshellarg
- Always use REST as it was designed. The HTTP methods we use like GET and POST aren't just a name for variables. They represent how the user's browser, any load balancers, proxy servers, firewalls, your webserver, and PHP will act.
- A GET request is designed for fetching information. This may be as simple as viewing your home page (with no path or arguments), or as complex as fetching the results for an advanced search form. The point is that when you make a search, you aren't changing any information on the server, just looking. If you stick to this, it means you can easily cache GET requests and speed up your server significantly when you grow.
- A POST is designed to make any changes to the server, wether its by creating a new record from a form submission (as most people use it), or simpler tasks like deleting a record, which you may think to create as a link, like users.php?delete=15, should always be a POST request. If you leave these as GET, you now can't cache GET requests without a fear of data loss, you risk search engines or browser optimizers automatically following these links and deleting accounts, and users don't get a confirmation dialog asking if they really want to do something again if/when they press the back button.
- A PUT request is designed to be the opposite of a GET, you send PUT /path/to/url then the data for a file, and all future GET requests to /path/to/url will return that data. This is what PUT was made to do, but that kind of upload itself rarely useful to most php developers. A POST request is an upload to a processing script, that will then figure out what to do with it, something thats FAR more common practice with PHP. The dangerous part is that people like to use PUT just like POST, when building API's and such. If you arent using PUT in the way I've described, please use POST instead. Doing otherwise can cause huge issues for people connecting to your API, as some engines will let them treat PUT like POST, some won't, and any engines built to talk to REST services won't understand your system correctly.