Why you should go with “secure by default” for your web application

You probably heard about it, web applications are notoriously insecure. By now, most web developers seem to be aware of the security issues, yet vulnerabilities are more common than ever. Some people say, it’s simply because developers tend to make mistakes. Other people say (and I agree) that wrong tools are being used which allow developers to make mistakes.

Case study #1: SQL injection

Yes, SQL injection still isn’t dead. Every now and then some web application makes the news with an SQL injection vulnerability, some dynamically composed SQL query which goes like:

db.query('SELECT * FROM table WHERE date > ' + request['date'])

You can bet on some self-proclaimed expert declaring in the comments: “That programmer is a noob! How could he forget to escape the variable?” But of course he could because he is merely a human. Code reviews help but occasionally such mistakes slip through review as well. But it’s not just that, maybe request['date'] was indeed safe at some point, e.g. validation code elsewhere made sure it is a number. But then somebody decided to allow more values for it and nobody thought about adapting the code here.

So, are we stuck forever with SQL injection vulnerabilities? As long as we rely on manual escaping we likely will be, it is just too error-prone. Instead, we can establish a policy to use prepared statements consistently throughout the codebase. SQL queries should never be composed dynamically, parameters should be inserted via placeholders instead:

db.execute('SELECT * FROM table WHERE date > ?', request['date'])

Here, request['date'] is being passed in as parameter for the query, the database engine will take care of inserting and escaping it properly all by itself. We don’t need to do anything, so there is nothing we could forget!

Case study #2: Shell injection

Shell injection is very similar to SQL injection, it is also an issue that only exists because people keep using the wrong tools. Consider the following code:

exec('/usr/bin/gzip file -' + request['quality'])

This code expects request['quality'] to be a number, but what if it is something like 0; rm -rf /? “Noob programmer, why did he forget to validate the parameter?” The answer is the same, human beings tend to forget things.

But the disaster here should be avoidable. All programming languages allow calling an application without going through the shell and specify the list of parameters explicitly:

exec(['/usr/bin/gzip', 'file', '-' + request['quality']])

What changed? Now '-' + request['quality'] is always a single parameter to the application, no matter what its value is. Allowing the user to pass in a random parameter to the application is still bad but usually it won’t cause any real harm.

But of course the side-effect is that all the nice stuff implemented by the shell won’t be available. Yes, you have to specify the full path to the application — there is no shell to locate it for you. Yes, you have to do variable expansion yourself. Yes, you have to route streams from one application to another “manually.” No, it’s not that hard and you don’t need the shell for that. On the bright side, your application won’t break when confronted with some unusual shell.

Case study #3: Cross-Site Scripting

Bored? Sorry about that but we are coming to the interesting part now. Getting rid of Cross-Site Scripting is much harder, simply because there are so many different ways to write vulnerable code, some of which are not entirely obvious. But the classic scenario is still the following:

<div>Your search for {{request.search}} produced no results.</div>

You can probably guess by now that I’m not going to advice quickly escaping that variable before somebody notices. If the web application isn’t something entirely trivial there will always be some variable left unescaped. Luckily, there is a better solution: modern template frameworks are capable of escaping automatically! For example, Django templates escape by default, same goes for Ruby on Rails 3.0. Jinja2 supports autoescaping as well, sadly not by default however.

When using any of these template engines the code above will be perfectly safe, escaping will be performed automatically. Even better, they will automatically prevent double-escaping:

{% macro greeting(name) %}
<div>Hey, {{name}}</div>
{% endmacro %}
{{greeting(name) + postfix}}

In this example only the name and postfix variables will be escaped. The result of the macro on the other hand will be automatically marked as safe and won’t be escaped.

Hey, but what about JavaScript code? The templates won’t help you with inserting into JavaScript code and that’s why you just shouldn’t do it. I’m serious, don’t do it! When inserting into JavaScript code there is simply too much that can go wrong. For example, is JSON safe?

<script>
{% autoescape false %}
var data = {{data|json}};
{% endautoescape %}
</script>

What if some value in your data is "</script><script>alert(/xss/)</script>"? It simply closed your script tag and created one of its own — all that while staying perfectly valid JSON code. The correct approach would take advantage of autoescaping:

<div id="my_data" hidden>{{data|json}}</div>
<script>
var data = JSON.parse(document.getElementById("my_data").textContent);
</script>

Not only is this code safe now, the new JavaScript code is static and can be moved into a separate file. And this in turn allows you to enable Content Security Policy and disable all inline scripts. Huge win for security!

Want another example? Inline event handlers are something where escaping is notoriously hard to get right (see also “More complicated XSShere).

<a href="#" onclick="goTo('{{request.url}}')">

It’s an attribute so autoescaping should do? No, it’s an attribute containing JavaScript code, so one would have to apply JavaScript-specific escapes before HTML escapes (yes, you have to do both and in that exact order). Rather than going down that rabbit hole, one can just use a regular attribute:

<a href="#" data-url="{{request.url}}" onclick="goTo(this.getAttribute('data-url'))">

That’s it, now nothing can go wrong any more. And again, since your inline event handler is static code now, that’s a good opportunity to move it into a separate JavaScript file. Did I mention Content Security Policy already?

Conclusions

I could go on, e.g. about using secure DOM methods rather than innerHTML or about banning eval(). Way too often security issues in web applications are addressed by adding more checks and hoping that adding these won’t be forgotten anywhere. I consider this the wrong approach, there should be fewer checks. Instead, you should stick to tools that will enforce security by default. You shouldn’t have to do more work in order to make things secure, it should rather take extra effort to build a security vulnerability.

There is an additional benefit here. How does one tell that the web application is free of obvious XSS issues? Without autoescaping you would have to go through every single variable inserted into a template and verify that all of them are escaped properly. Autoescaping makes this tedious task unnecessary, all you need to check are the (hopefully few) cases where autoescaping has been explicitly disabled. Then you have to verify that no variables are being inserted into JavaScript code and you are already done.

Comments

  • Stephan Sokolow

    “Yes, you have to specify the full path to the application”

    …unless their programming language exposes the PATH-resolving exec*p*() variants of exec() or a wrapper which can use them like Python’s subprocess module.

  • Ben Basson

    I agree with the sentiment that developers are introducing vulnerabilities by using the wrong tools. I would also add that they are not actively looking for vulnerabilities themselves.

    Developers should be thinking like attackers and doing their best to break their own software. Further to that, they should be getting their software independently tested by experts, or failing that, at least scanning for vulnerabilities using the very extensive tool set out there. There’s really no excuse these days.