Scott Smith

Blog Tutorials Projects Speaking RSS

Secure Node Apps Against OWASP Top 10 - Injection

Welcome to part 1 of the OWASP security series

  1. Injection
  2. Broken Authentication & Session Management
  3. Cross Site Scripting (XSS)
  4. Cross Site Request Forgery (CSRF)
  5. Using Components with Known Vulnerabilities (Coming soon)

In this multipart series, we will explore some of the the OWASP top web application security flaws including how they work and best practices to protect your application from them. The focus will be on Express web applications in Node, but the principles shown can be applied to any framework or environment.

This part of the series will cover Injection.

Injection

So what exactly is an Injection attack? An Injection attack occurs when an attacker sends text-based attacks that exploit the syntax of the targeted interpreter.

An attacker can be anyone capable of sending untrusted data to the system such as external users, internal users, administrators, etc.

Injection attacks can be very bad for an application. They can result in data loss, data corruption, data access, denial of access, and even complete takeover.

SQL Injection

When you hear about Injection attacks, the one you might think of first is SQL injection. SQL injection has been around for almost 20 years and is still a big issue for many web applications. A study done in 2012 by Imperva observed that average web applications get at least 4 SQL injection attacks per month.

Most commonly known as an attack vector for web applications, SQL injection can also be used to attack any application using SQL databases. Like Injection attacks in general, SQL injection is done by injecting code (via text) into data-driven applications. The goal of these attacks is to inject SQL statements and have them executed for the purpose of dumping the database contents, deleting data, and more.

Some of the systems that can be affected by this attack are SQL Server, PostgreSQL, MySQL, and any SQL based database.

Below you will see an example Express application using a MySQL database that is vulnerable to SQL injection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var express    = require('express');
var bodyParser = require('body-parser');
var mysql      = require('mysql');
var connection = mysql.createConnection();

var app = express();

app.use(bodyParser.urlencoded());

app.post('/login', function (req, res) {
  var user = req.body.user;
  var pass = req.body.pass;
  var sql  = "SELECT * FROM users WHERE user = '" + user + "' AND pass = '" + pass + "'";

  connection.query(sql, function(err, results) {
    // ...
  });
});

app.listen(80);

The problem with this code is that we are building our SQL statement using user input. You can see this on lines 11 to 13. We are reading in user supplied input for the user and pass variables. These are then used via string concatentation to build our SQL statement.

So why is this bad? Let me show you an example of how bad this is. Imagine someone making the following HTTP request to the /login endpoint.

1
2
3
4
5
POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

user=admin'--&pass=whatever

The resulting SQL statement would look like this.

1
SELECT * FROM users WHERE user = 'admin'--' AND pass = 'whatever'

Yeah, that is very bad. Assuming there is an account named admin, this attacker would now have access and permissions that the admin account has. But this is only a simple example. The attacker could do a multitude of things given this vulnerability.

Solution #1: Escape user input

In this example, we are using the mysql npm package. It offers functionality to properly escape user input which we can use to lock down our application. Many other libraries out there offer this type of functionality or you can find libraries solely for escaping user input. Here is our previous example now escaping user input.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var express    = require('express');
var bodyParser = require('body-parser');
var mysql      = require('mysql');
var connection = mysql.createConnection();

var app = express();

app.use(bodyParser.urlencoded());

app.post('/login', function (req, res) {
  var user = connection.escape(req.body.user);
  var pass = connection.escape(req.body.pass);
  var sql  = "SELECT * FROM users WHERE user = '" + user + "' AND pass = '" + pass + "'";

  connection.query(sql, function(err, results) {
    // ...
  });
});

app.listen(80);

Now, when someone sends an HTTP request like our previous example, the resulting SQL statement will look like this.

1
SELECT * FROM users WHERE user = 'admin''--' AND pass = 'whatever'

You can see that the resulting SQL statement no longer has the exploit in it.

While escaping user input is better than not, it is still not full proof. Escaping algorithms can have bugs, not cover all cases, or miss newly found exploits.

Solution #2: Parameterized SQL queries

Parameterize SQL queries is an even better way to secure your application. Instead of building a SQL statement using concatenation, we let a function replace the parameters within the statement and perform sanitation. Here is what our example would look like using this method. Please note this is a bit pseudo code but helps get the point across.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var express    = require('express');
var bodyParser = require('body-parser');
var db         = require('db');

var app = express();

app.use(bodyParser.urlencoded());

app.post('/login', function (req, res) {
  var user = req.body.user;
  var pass = req.body.pass;
  var sql  = "SELECT * FROM users WHERE user = $1 AND pass = $2";

  db.query(sql, [user, pass], function(err, results) {
    // ...
  });
});

app.listen(80);

What happens here is $1 and $2 get replaced with user and pass when we make the call to db.query. With our previous example, this is what the SQL statement would look like.

1
SELECT * FROM users WHERE user = 'admin''--' AND pass = 'whatever'

Node Injection

SQL is not the only way in which an injection attack can occur. Node can also be an attack vector. This can be done by getting it to execute JavaScript submitted as user input.

Attacks are done by taking advantage of applications that use eval() with user input. The following code shows an Express application vulnerable to this type of attack.

1
2
3
4
5
6
7
8
9
10
11
12
var express    = require('express');
var bodyParser = require('body-parser');

var app = express();

app.use(bodyParser.urlencoded());

app.post('/run', function (req, res) {
  eval(req.body.cmd);
});

app.listen(80);

If someone were to make an HTTP request like the following, things would be bad in the application. It would be stuck in an infinite loop outputting to the console.

1
2
3
4
5
POST /run HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

cmd=while(1){console.log("HACKED")}

This is a more benign exploit example but if this hole existed in an application, an attacker could execute anything they want.

The solution here is pretty simple. Never use eval() with user input. If you have to, be aware of the risks and attempt to mitigate them as much as possible.

MongoDB Injection

Another type of injection attack you need to be aware of is when working with MongoDB. These attacks usually take advantage of query selectors or the fact that they do not get explicitly set. The following Express application currently has a vulnerability to this type of attack.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var express    = require('express');
var bodyParser = require('body-parser');

var app = express();

app.use(bodyParser.urlencoded());
app.use(bodyParser.json());

app.post('/login', function (req, res) {
  var user = req.body.user;
  var pass = req.body.pass;

  db.users.find({user: user, pass: pass});
});

app.listen(3000);

The exploit occurs because of the couple things. First, we are allowing JSON data to be posted to our application. Second, we are not specifying the query selectors to use (line 13). If an attacker were to make the following HTTP request to our application, a successful attack would occur.

1
2
3
4
5
6
7
8
POST /login HTTP/1.1
Host: example.com
Content-Type: application/json

{
    "user": {"$gt": ""},
    "pass": {"$gt": ""}
}

What happens here is the attacker is passing JSON objects for the user and pass parameters. When those are passed in to the db.user.find() call, the following occurs.

1
db.users.find({user: { '$gt': '' }, pass: { '$gt': '' }});

When this executes, we will find all users with user greater than ” and pass greater than ”. This will return all users within the user table. This happened because we are not explicitly setting the query selector so the attacker was able to specify one themself.

The solution to this exploit is simple. You need to explicity set the query selector. Here is our previous code updated to fix this issue. See line 13 for the changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var express    = require('express');
var bodyParser = require('body-parser');

var app = express();

app.use(bodyParser.urlencoded());
app.use(bodyParser.json());

app.post('/login', function (req, res) {
  var user = req.body.user;
  var pass = req.body.pass;

  db.users.find({user: { $in: [user] }, pass: { $in: [pass] }});
});

app.listen(3000);

Here is what the result is now if someone were to make the same exploit HTTP request.

1
db.users.find({user: { $in: [{ '$gt': '' }] }, pass: { $in: [{ '$gt': '' }] }});

Now, all users will not be returned for this attack and our code works as expected.

Wrap up

Injection attacks are one the most prevalent attacks out there, easiest to exploit, and can have a severe impact. If you can take one thing away from this article is to never trust user input. It is the trust in the input that allows these attacks to occur.

I have a lot more tutorials coming so be sure to subscribe to my RSS feed or follow me on Twitter. Also, if there are certain topics you would like me to write on, feel free to leave comments and let me know.