Security

General principle

Never trust user input

All information coming from the user must be checked

Example

You will find sample code in the never_trust_user_input folder of the archive.

Can you spot a security issue in this code?

Suppose a user is buying items on an e-commerce website. At the end, they are offered the option to donate to a charity with this form:

<form method="post">
    In addition to your purchase, would you like to donate
    to a charity?
    <select name="charityPrice">
        <option value="0">No thanks</option>
        <option value="10">10 €</option>
        <option value="50">50 €</option>
        <option value="100">100 €</option>
    </select>
    <input type="submit" />
</form>

And that, on the server side, we compute the final price they will have to pay:

<?php
// ...
$totalPrice = $basketPrice;
if ($_POST['charityPrice']) {
    $totalPrice += $_POST['charityPrice'];
}

Here:

  • Unless you explicitly write code to do so, nothing checks that the value chosen by the user was one of the values offered in the <select> menu
  • What if the user forged a request in which charityPrice was a negative number? We would end up subtracting from the total price!

Here, the problem can easily be fixed by checking on the server side (in PHP) that the user actually provided one of the offered values.

In other words, do not trust the POST data coming from the user.

SQL injection

Sample code is available in the sql_injection folder of the archive. You will need to edit pdo.php to connect to the shows database.

Can you spot a problem?

If you remember what was said in the lecture, the problem comes from this line:

<?php
$query = $pdo->query('SELECT title, plot FROM series WHERE id='.$_GET['id']);

Never do this!

The problem with this code is that it also lets the user alter the structure of the query itself.

An example of exploit here would be to provide the following value for the GET parameter id:

show.php?id=99999999 UNION SELECT email, password FROM user LIMIT 1

The query (re-indented) then becomes:

SELECT title, plot
FROM series
WHERE id=99999999
UNION
SELECT email, password
FROM user
LIMIT 1

Here:

  • We pick an id, 99999999, that most likely does not exist
  • We create a union with the user table, from which we retrieve a user's email and password

This attack requires finding out the names of the tables, fields, etc. But this work can be automated!

To prevent this vulnerability, simply use a prepared statement.

XSS and CSRF

Now look at the code in xss_csrf, which will be used for this part and the next one. You will also need to configure pdo.php, and make sure there is a user in the database whose id matches $userId in index.php.

Can you see anything dangerous?

XSS

The first vulnerability here is an XSS (Cross-Site Scripting) vulnerability. The problem is that the content of the comments is not "escaped" when they are displayed in the HTML page.

For example, if you enter a comment such as:

<b>Hello</b>!

The text will appear in bold.

The problem is that this also lets you enter a comment such as:

<script>document.location='https://google.com';</script>

From now on, the page will redirect you to https://google.com!

This vulnerability allows much worse things, in particular stealing the session cookie, but you already get the idea.

To fix this vulnerability, you need to escape the data coming from the database when it is displayed, for example using the htmlspecialchars() function.

When you use a template engine such as Twig, values are escaped by default. That is, it is as if htmlspecialchars() wrapped all your outputs. If you really want to display HTML, you can use the raw filter.

CSRF

There is a second vulnerability on this page, which is much harder to think of.

Imagine someone builds this form:

<form method="post" action="http://your-website.com/index.php">
<textarea rows="5" cols="100" name="comment">I love this show!</textarea>
<br/>
<input type="submit" value="Send" />
</form>

Where the URL in the action attribute points to your production website.

This form could even be enhanced with an automatic submission triggered in JavaScript when the page loads:

<body onload="post()">
<script type="text/javascript">
function post() {
    document.getElementById('form').submit();
}
</script>

You will find this example in exploit.html

Such a form can be hosted anywhere, for example on a website run by the attacker, who could then force you to submit arbitrary data to the form in question.

The problem is that when submitting the form, you will also send your session cookie and will be recognized by the server!

Fixing this vulnerability is a bit more technical, and takes several steps:

  • Add a hidden field to the form, usually called csrf_token
  • The content of this field is randomly generated and also stored in the session
  • When the form is submitted, check that the value of the CSRF token matches the one stored in the session

This work is handled automatically by a web framework.

Passwords and hashing

Look at the source code in the password_hash folder.

Does anything strike you?

Here, the password super_password is stored in plain text in the file:

if ($_POST['password'] === 'super_password') {

Suppose the server is compromised (say another vulnerability gives someone read access to the data). They will then know the password.

The same principle applies if the password is stored in the database.

A password must never be stored in plain text!

Instead, we use a hash function, for example the password_hash() and password_verify() functions in PHP.