Retired Documentation: You are using the documentation for version 1.7.3 which was retired in 2013. Go here for the latest version documentation or check here for your available upgrades to the latest version.
Security Guidelines
Table of Contents
- Cross Site Scripting (XSS)
- SQL Injection Prevention
- Preferences and Settings
- Tag Parameters
- Secure Forms
- Handling Form Submissions
- Filename Security
- Typography Class
- General Security Practice
Guidelines
Cross Site Scripting (XSS)
Cross Site Scripting is a type of security vulnerability that allows code injection by malicious users onto a page. You can find some educational reading and examples on the following site: http://ha.ckers.org/xss.html
Cross Site Scripting should be taken very seriously as you would never want your add-on to be the source of an attack vector.
-
$REGX->xss_clean()
$REGX->xss_clean() is the built in ExpressionEngine XSS sanitization method, which is constantly tweaked for improved security and performance. It accepts both a string and an array and will return sanitized text.
$str = $REGX->xss_clean($str);
-
Sanitized Variables on Input
Keys are cleaned and data sanitized automatically by the Input Class for the following:
- $_GET
- $_COOKIE
- $IN->IP
- $IN->AGENT
- $IN->URI
Keys are cleaned and the data is not sanitized automatically by the Input Class for the following:
- $_POST
- $_SERVER
This means that any front-side user input from these two sources should be sanitized by using $REGX->xss_clean() before inserting into the database our outputting to the screen. If the user input is in the Control Panel, such as a module's back end, it is at your discretion based on the needs of the module for whether or not administrative input is sanitized. Always err on the side of caution, and never assume that your end user will only allow access to the back end of your module to trusted members.
-
INSERT and UPDATE Queries
As ExpressionEngine assumes that all information in the database is sanitized against XSS, the responsibility for sanitization falls on input to the database. Therefore, all INSERT and UPDATE queries should use $REGX->xss_clean() for user input data.
-
Outputting Data to the Page
Use the Typography class when outputting to a page. Doing so provides a number of benefits and security precautions:
- Allows Safe HTML or no HTML at all if desired
- Validates all pMcode, such as [url] and [img] to keep them safe
- Encodes ExpressionEngine tags and variables by default
-
When In Doubt…
If there is any doubt on the safety of a variable that you are outputting or inserting into the database, use XML Encode ($REGX->xml_encode) or htmlentities() and strip_tags().
SQL Injection Prevention
SQL Injection is a special type of attack in which data is used in a query without being properly filtered, allowing a user to execute their own queries on the database. Example:
$evil = "brett'; DELETE FROM exp_members;";
$query = $DB->query("SELECT * FROM exp_members WHERE username='{$evil}'");
For more information, you can read MySQL's guide to SQL Injection security: http://dev.mysql.com/tech-resources/articles/guide-to-php-security-ch3.pdf
-
Escaping PHP Variables
PHP variables should be escaped in queries anytime the variable is not explicitly set to a hard-coded value within the method using the query. This means that even variables passed as arguments to a method must be escaped before being used in a query.
Manually written queries should use $DB->escape_str() on variables, even if you think the value is trusted.
$query = $DB->query("SELECT field FROM table WHERE column = '".$DB->escape_str($foo)."'");
$DB->insert_string() is the preferred method for INSERT queries, as values are escaped automatically in the supplied data array.
$data = array('name' => 'Brett Bretterson', 'email_address' => '[email protected]'); $DB->query($DB->insert_string('table', $data));
$DB->update_string() is the preferred method for UPDATE queries, as values are escaped automatically in the supplied data and "where" arrays.
$data = array('email_address' => '[email protected]'); $where = array('name' => 'Brett Bretterson'); $DB->query($DB->update_string('table', $data, $where));
NOTE: If you send the third argument (the WHERE clause) as an array as shown above, it will automatically be escaped. If you send a string, you must escape it yourself.
$data = array('email_address' => '[email protected]'); $DB->query($DB->update_string('table', $data, "name = '".$DB->escape_str($foo)."'"));
Preferences and Settings
-
Storage of Settings
Security and required preference settings should be stored in the database or config.php file.
-
Use of Settings in Forms
Never send values for preferences or settings in hidden form fields. HTML source is open and readable, so a malicious user could simply copy the HTML or use a browser plugin to alter the form data to something you do not expect or desire. If absolutely required, encode them:
- JavaScript is good against bots but not against serious hackers.
- Base 64 encoding is easy to break and therefore NOT recommended.
- If there are a limited number of possible values, you could use md5() or sha1() to encode the values and check against encoded possible values. This is not bulletproof of course, as the hacker needs only to know what the possible values are to be able to utilize them.
- PHP has the Mcrypt library and other PHP libraries which have encryption and decryption with a salt. CodeIgniter has an Encryption class, incidentally.
-
Yes / No Preferences
If your preference setting is a simple Yes / No, use 'y' for Yes and 'n' for No in both the code and the database, to keep things simple and consistent.
-
Follow the Art of KISS
"Keep It Simple, Stupid". Before adding a preference, ask yourself: is a preference for 'foo' really needed? Eventually with too many preferences, there will be interference and priority issues, and overcomplication.
Tag Parameters
-
Never Assume Tag Parameters are "Good" Input
Do not make security exceptions for tag parameters. With PHP on Input, nested tags, other plugins, or variables being possible sources for parameter values, you cannot be sure that the data is safe.
-
Validate Values Before Using
Always validate the values being supplied to a tag parameter before using them in your code. switch() statements are good for numerous possible values, as are arrays of possible values:
switch ($foo = $TMPL->fetch_param('foo')) { case 'bar': case 'baz': case 'bag': // value is already set, and okay, so simply break break; default: $foo = ''; break; }
$valid_foo = array('bar', 'baz', 'bag'); $foo = (in_array($foo = $TMPL->fetch_param('foo'), $valid_foo)) ? $foo : '';
If you cannot validate against specific values, at least validate the type of data.
if (preg_match('/[^0-9]/', $foo = $TMPL->fetch_param('foo'))) { $TMPL->log_item('Super Class Module error: Provided parameter "foo" contains non-digit characters'); return FALSE; }
Or even:
$foo = (preg_match('/[^0-9]/', $foo = $TMPL->fetch_param('foo'))) ? FALSE : $foo;
SIDE NOTE: You no doubt notice that a regular expression is being used here to validate the parameter as a numeric value. Why? is_numeric() returns TRUE for some non-integer numbers, including notation, e.g. "-0123.45e6". is_int() only returns TRUE on actual integer variable types, and tag parameters are always strings. Lastly, ctype_digit(), the function that we are actually emulating here, is sadly not readily available on all servers, so we avoid using it.
-
Default Values
Always have default values if you plan to allow the code to execute without parameters being supplied, or in the case of invalid parameter values being provided. An empty string, NULL, or boolean FALSE simply needs to be tested later to accommodate defaults in your code. This also allows you to change the defaults all in one place in the script. Here is one method, that takes advantage of PHP's variable variables.
$defaults = array( 'type' => '', 'show_foo' => FALSE, 'limit' => 5 ); foreach ($defaults as $key => $val) { $$key = ($$key = $TMPL->fetch_param($key)) ? $$key : $val; } // Results in three variables being set: // $type, $show_foo, and $limit, to their corresponding tag parameter value // or the default value if the parameter was not present // Each variable would still need to be validated as instructed above // before using them in the code.
Secure Forms
To help prevent spam and protect against Cross-site Request Forgery (CSRF), ExpressionEngine has a "Secure Form" setting that uses a hash stored in the database tied to the IP address of the machine that the form was generated for. Here is how to make use of it.
-
$FNS->form_declaration()
Create all forms on the user side with $FNS->form_declaration(), so the XID (secure hash ID) is added automatically as a hidden input field. This also allows any extensions the site may have installed that modifies forms to have effect on your forms.
-
Handling Form Hashes in Your Add-on
Do a check and deletion for the secure hash after all error checking has been completed, but prior to any data insertion into the database. Example:
// error checking and data validation before this! if ($PREFS->ini('secure_forms') == 'y') { $query = $DB->query("SELECT COUNT(*) AS count FROM exp_security_hashes WHERE hash='".$DB->escape_str($_POST['XID'])."' AND ip_address = '".$IN->IP."' AND date > UNIX_TIMESTAMP()-7200"); // keep hash valid only for 2 hours if ($query->row['count'] == 0) { // no data insertion if a hash isn't found or is too old $FNS->redirect(stripslashes($_POST['RET'])); } else { $DB->query($DB->insert_string('table', $data)); $DB->query("DELETE FROM exp_security_hashes WHERE ( hash='".$DB->escape_str($_POST['XID'])."' AND ip_address = '".$IN->IP."' ) OR date < UNIX_TIMESTAMP()-7200"); // helps garbage collection for old hashes } } else { $DB->query($DB->insert_string('table', $data)); }
-
Forms in the Control Panel
The Control Panel's Display class automatically adds hashes to any form tag automatically for you. Likewise, the system will check for hashes automatically, so forms in the control panel require no additional work for you to use securely.
Handling Form Submissions
Form submissions are the most common form of user input you will handle in your add-ons, so it is important to understand how to deal with them securely.
-
Outputting Form Data to the Screen
Never output unfiltered incoming data directly to the screen.
-
Trust No One
Treat all input as potentially dangerous, even from within the control panel.
-
Use a Logic Map for Processing
In your methods that will be handling form data, create a logic map that you can use to ensure that you are handling all validation and security checks prior to performing any actions. The following list contains common things to use; your add-on may have fewer or additional requirements.
- What is validated and in what order?
- Does the user need to be a logged in member?
- Does the user need to be in a specific member group for the action?
- Deny Duplicate Data Check?
- What security checks are performed?
- Secure form hashes
- CAPTCHA
- Blacklist Banning / Whitelist Overrides
- $IN->blacklisted == 'y' (blacklisted)
- $IN->whitelisted == 'y' (whitelist override)
- Preferences and settings checked against
- Data Filtering and Conversion
- XSS clean
- Number formatting: number_format(), ceil(), etc.
- Character set conversion
- XML convert
- Remove PHP or ExpressionEngine tags?
- Insert Data or Update
- $REGX->xss_clean() on all string data even if there is no intent to output (don't forget about the Query module!)
- Make sure all data is properly escaped
After processing, make sure submitted data that might be sent to the screen for a success or error message is the filtered and validated version
- What is validated and in what order?
Filename Security
-
include() and require()
Many servers have the ability to include files from offsite or anywhere in the local server, so when using include() or require() with user submitted data you need to be extremely careful. The best practice is to not design your add-on in such a way that would make this necessary in the first place, but if you do, either:
- Validate the filename based on possible options, OR
- Use $FNS->filename_security() to remove naughty characters
-
Saving Images or Files to the Server
When saving images or files to the server, make sure and validate the file type (MIME) and also clean the file name to remove possible naughty characters.
- Sanitize file name: $FNS->filename_security();
- Browser provides the MIME type, available in: $_FILES['userfile']['type']
- Use ExpressionEngine's Upload class in core.upload.php whenever possible, as it contains methods for validation and sanitizing
Typography Class
Use the Typography class whenever outputting blocks of content from user submitted data. It is regularly updated to improve security and performance, saving you both time and energy.
- It protects against PHP and ExpressionEngine tags from being parsed
- pMcode is sanitized, even if Allow All HTML is enabled
- Using 'safe' or 'none' for HTML formatting further protects output by converting tags to entities
General Security Practice
- Super Admins' absolute power is for access, not security. Do not make security exceptions for Super Admins. "Doom, doom, doom," as it were.
- Imagine a Super Admin not logging out from a public terminal or not using an SSL connection on an open wireless network.
- Imagine a Super Admin using Cookies Only sessions in the control panel and then going to a third-party page, which automatically submitted a form with data to the entry submission routine in the control panel. Theoretically, the Super Admin would be submitting potentially malicious code into an entry automatically and without any knowledge.
- Use built in ExpressionEngine classes and methods if they exist for tasks.
- Use good beta testers and run a tight ship to get the best results.
- Keep debugging on for all users on your private development / testing site. Refer to the instructions for PHP errors in the General Syntax and Style section.
- Use an approach of Least Privilege. Start by allowing access to NO one, and explicitly grant access to those that qualify.