Cover Photo

Four Tips for a More Secure Website

Jan. 8, 2018 • 3m 30s
security
webdev

Security is a hot topic in web development with great reason. Every few months a major website is cracked and millions of user records are leaked. Many times the cause of a breach is from a simple vulnerability that has been overlooked. Here are a few tips to give you a quick overview of standard techniques for making your websites more secure. Note: I do not guarantee a secure website if you follow these suggestions, there are many facets to security that I don’t even touch in this article. This write-up is for increasing awareness about techniques used to correct some common vulnerabilities that appear in web applications.

1. Parameters are good for your health

According to OWASP, the top vulnerability for web applications is SQL injection. What is SQL injection? It is user provided data embedded into a SQL query without any protection. Here’s an example in PHP:

<?php

$connection = new PDO(...);
$username = $_POST['username'];
$query = "SELECT username, password FROM users WHERE username = '$username'";
$connection->query($query);

Your next question might be, what is wrong with that code? It is the way the username variable is used inside the query variable. The username comes from an untrusted source and can be manipulated by a hacker to change the query. What if the posted value for username was actually jdoe’ AND 1=1? The query would then become SELECT username, password FROM users WHERE username = ‘jdoe’ AND 1=1 because we are using the $username variable directly in the query string. A malicious party could then inject any kind of queries into your database and leak private data.

So what is the way to prevent this? Database vendors created a solution called parameterization. It allows programmers to send a query to the database and then provide the data later. The initial query is stored by the database as a template and then the data is bound to the template afterwords for execution. Since the data is separate, it will not be evaluated as part of the query. Here is the above example corrected:

<?php

$connection = new PDO(...);
$username = $_POST['username'];

$query = "SELECT username, password FROM users WHERE username = :username"
$statement = $connection->prepare($query);
$statement->execute(['username' => $username]);

If a hacker submits jdoe’ AND 1=1 as the username, then the text will just be evaluated as part of the comparison and not executed as a query. The parameterized query would work similar to this: SELECT username, password FROM users WHERE username = '\'jdoe\' AND 1=1'.

I recommend using parameterized queries for any query, regardless of if it uses untrusted data or not. In many cases, it can make your queries run faster as well. Most popular ORMs use parameterization, so choosing a good ORM for your team can save you a lot of headache.

2. Escaping Isn’t as Hard as it Sounds

The next most popular website exploit is called cross site scripting or XSS. If a website is vulnerable to XSS, then user supplied JavaScript can be executed on the site. In a typical web app, you only want your website’s JavaScript to be executed on the browser. However, if you are not careful, malicious JavaScript can be run from data submitted by a user through parameters or forms. Here is an example:

<h1><?php echo $_GET['title']; ?></h1>

In this page, we are using a title GET parameter to display the title of the page. So, we could open /index.php?title=Hello and see the word Hello as the page header. Works great, but what if someone entered some JavaScript as the title? Opening /index.php?title=<script>alert('Hello world');</script> would print the script on the page and trigger an alert to open! The danger of XSS is that a hacker could use malicious JavaScript to trick your users, create spam or even steal form data such as usernames and passwords.

The best way to prevent XSS is to escape any HTML characters in the output on the page. “Escaping” converts special characters into equivalent HTML codes that prevent the characters from being interpreted as HTML. For example, the character < would normally be read in a browser as part of an HTML tag. However, when it is escaped, it becomes &lt; which tells the browser to output < to the page, but not run it as HTML. Now, the fixed example:

<h1><?php echo htmlspecialchars($_GET['title'], ENT_QUOTES, 'UTF-8', false); ?></h1>

PHP will now convert any special characters into their equivalent HTML entities, making the output safe for user input. The text returned by the server looks like: <h1>&lt;script&gt;alert(&#039;Hello world&#039;);&lt;/script&gt;</h1>. As you can see, all the HTML tags were converted, preventing the script tag from being run.

In most modern frameworks, entity conversion happens automatically as part of the templating process. Django Templates, Erubis or Blade will handle the conversion for you so you do not have to worry about it. However, it is good to be aware of the issue in case you ever need to print user data directly to an HTML page.

3. Don’t Forget to Sanitize when Cooking Text

Your next question might be, “what do I do when I want to show user supplied HTML in a webpage?”. This is a very common scenario on CMS sites. A user might write up a blog post with a bunch of HTML characters for formatting text that need to be kept when outputted on the page. At the same time though, we don’t want bad JavaScript to be embedded in the blog post. The solution is sanitization.

Sanitization is the process of cleaning text to remove/escape bad JavaScript, but leave good HTML tags. A popular Python sanitizer is called Bleach (created by Mozilla). Here it is in use:

import bleach

post_text = (
    "<h1>Hello</h1>\n"
    "<script>alert('Hello world');</script>"
)

clean_text = bleach.clean(
    post_text,
    tags=['h1']
)

print(clean_text)
# Outputs:
# <h1>Hello</h1>
# &lt;script&gt;alert('Hello world');&lt;/script&gt;

In the example, we were able to preserve the h1 tag by passing it as an acceptable tag to Bleach, but Bleach also escaped the script tags making our string safe from JavaScript. This string is now safe to be output on an HTML page. Typically, sanitization is used before storing data in the database since it can be a slower process than normal template escaping.

If you ever need to accept text with HTML, sanitation is your best option. Every language has a good sanitizer, including HTML Purifier for PHP, Loofah in Ruby and the previously mentioned Bleach for Python.

4. Surf to Safer Waters with CSRF Tokens

Cross Site Request Forgery (otherwise known as csurf) is caused by one website submitting a malicious form to a different website on behalf of the current user. Let’s say that you have a banking web app for transferring money to another account. Something like this:

from django.shortcuts import redirect
from .models import Transfer

# https://mybank.com/transfer/create
def transfer_money(request):
    account_num = request.POST['account_num']
    money_total = request.POST['money_total']

    transfer = Transfer(account_num=account_num)
    transfer.complete(total=money_total)
      transfer.save()

    return redirect('transfer-index')

And a form that submits to that route:

<form action="https://mybank.com/transfer/create" method="POST">
    <label>Account Number</label>
    <input type="text" name="account_num" />

    <label>Transfer Total</label>
    <input type="text" name="money_total" />

    <button type="submit">Submit</button>
</form>

This route and form allows the banking user to send funds to another account by supplying the destination account number and the money total to be transferred. However, what happens if a different website submits a form to this URL? What if a hacker has created mybankk.com/transfer and that page has a form that submits to mybank.com/transfer/create? Something like this:

<form action="https://mybank.com/transfer/create" method="POST">
    <label>Account Number</label>
    <input type="text" name="account_num_fake" />
    <input type="hidden" name="account_num" value="10000001" />

    <label>Transfer Total</label>
    <input type="text" name="money_total" />

    <button type="submit">Submit</button>
</form>

The browser will block this form request right? Not at all. The form will be accepted by mybank.com without issue, if the user who submits the form is already logged into mybank.com. Many hackers use this technique to trick users into making actions that they did not intend. In this case, an unsuspecting user might use mybankk.com/transfer to make a bank transfer, but inadvertently make the transfer to account # 10000001. Since the hacker has control over the account number, he can make the transfer go to any account that he desires.

The solution to this vulnerability is to use form tokens. Form tokens are randomly generated string tokens that are stored in your user’s sessions. When you generate a form from your website, you will embed the token as a hidden field in the form. Then, when the form is submitted, your web app can verify the form token matches what’s in the user’s session.

<!-- mybank.com transfer form -->
<form action="https://mybank.com/transfer/create" method="POST">
    <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}" />

    <label>Account Number</label>
    <input type="text" name="account_num" />

    <label>Transfer Total</label>
    <input type="text" name="money_total" />

    <button type="submit">Submit</button>
</form>

If mybankk.com submits its form, it will not have the token or will have an incorrect token so then the request will be rejected by mybank.com.

Most web frameworks provide CSRF tokens, however, the feature is not always on by default. Before moving an app to production, always verify CSRF tokens are being used for data changing HTTP methods like POST, PATCH and DELETE.

Conclusion

Proper security is really tough to maintain in a web app. There are a ton of vulnerabilities to prevent. I hope this article helped you begin to understand some of the techniques used in web security. Here are a list of resources that helped me during my research:

If you have any security tips, please leave them in the comments!