DEV Community

Cover image for Four Tips for a More Secure Website
Andrew Davis
Andrew Davis

Posted on

Four Tips for a More Secure Website

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);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

Category:OWASP Top Ten Project - OWASP
Testing for SQL Injection (OTG-INPVAL-005) - OWASP
SQL Injection Attacks by Example
Cross-site Scripting (XSS) - OWASP
Cross-Site Request Forgery (CSRF) - OWASP

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

Latest comments (6)

Collapse
 
sixolisemaboza profile image
Sixolise Maboza

Thank you for this, I'll read up more on SQL injection, I have been using parameterization but casually not knowing that the data that you bind will not be evaluated as part of the query.

Collapse
 
olivedev profile image
olivedev

For prevention of php sql injection attacks, it is best to go with prepared statements or pdo. You can also prevent it by making user's input to be authenticated.

Collapse
 
kchikwa profile image
be_happy

Really enjoyed reading this,especially about form tokens, anywhere i can learn more on form tokens and webapp security ?

Collapse
 
dvhh profile image
dvhh

because of performance issue for MySQL and prepared statement. it is possible that the prepared statement are ( unfortunately ) emulated ( see PDO::setAttribute and PDO::prepare ) .

I would recommend setting PDO::ATTR_EMULATE_PREPARES to false as much as possible.

Collapse
 
restoreddev profile image
Andrew Davis

Based on what I have read, emulated prepares are as safe as native prepares if you use them correctly. Though turning them off is a safer choice from a configuration perspective because it forces SQL to do the work. Setting up PDO could be a topic of its own post.

Collapse
 
dvhh profile image
dvhh

I am not aware of the internal implementation, but I still feel that native prepared statement is way safer than emulated one ( as the only obvious implementation would rely on some form of input escaping, which is not always working as intended )