Łukasz Makuch

Łukasz Makuch

Are injection attacks still a problem in JavaScript?

Back in the days when web development was all about server-side applications querying relational databases and outputting HTML, we saw many examples of code like this:

// CAUTION: Bad example!
function popup(msg: string): string {
    return "<p class=\"popup\">" + msg + "</p>";
}

or like this:

// CAUTION: Bad example!
function getName(login: string): string {
    return "SELECT name FROM users WHERE login = \"" + login + "\""
}

Since then we learned that there are safer ways to do this.

Template engines and parameter binding are commonly used tools. It's rare nowadays to see dangerous string concatenation.

In this post I'd like to share my observation on injection attacks. It seems they are still a problem in JavaScript!

To understand why let's break one of the bad examples down into more basic forms. Here it is:

function f(userInput: A): A {
    const firstCommand: A = ...;
    const secondCommand: A = ...;
    return firstCommand.concat(userInput.concat(secondCommand));
}

As we can see, the root cause of injection attacks is that for the computer there's no difference between a command and the user input! That's why a malicious user is able to enter data which is treated as code.

Of course, as I mentioned earlier, there are well known ways to protect against that type of attacks. Instead of writing this:

"SELECT name FROM users WHERE login = \"" + login + "\""

we would write something like this:

query("SELECT name FROM users WHERE login = :login", {login})

That way the command SELECT name FROM users WHERE login = :login is clearly separated from the data {login}. In the same time the underlaying mechanism makes sure that the data is prepared to be used in an SQL query. There's no way to escape the quotes and inject any malicious code.

However, the web development train is moving fast. What we can see more and more often is not only this:

{
  paramA: "the value of the A parameter",
  paramB: "the value of the B parameter",
}

but also this:

{
  paramA: "the value of the A parameter",
  paramB: {$in: [
    "the value of the B parameter",
    "the value of the C parameter",
  ]},
}

The huge difference between these two is that the parameter value is an object which consists of a command!

Let's say the values are read from the userInput:

{
  paramA: userInput.paramA,
  paramB: {$in: [
    userInput.paramB[0],
    userInput.paramB[1],
  ]},
}

We don't need to worry about the user providing a malicious string. It will all be safely handled.

The problem is that the parameter value doesn't need to be a simple value like the value of the A parameter but it also may be a command like {$in: ["B", "C"]}. Taking into consideration the fact that there are at least few ways how the user may send a request which when decoded gives us an object (a form, JSON or XML payload), the code is vulnerable to injection attacks.

Let's say userInput.paramA equals {$empty: false}. It makes the query look like this:

{
  paramA: {$empty: false},
  paramB: {$in: [
    userInput.paramB[0],
    userInput.paramB[1],
  ]},
}

Again, there's no way the computer can distinguish trusted commands from untrusted user input. Just instead of mixing trusted and untrusted strings, we're mixing trusted and untrusted objects.

The solution is to always write commands in such a way that they cannot be received from the user. One possible way to achieve this is the approach used by React and Snabbdom-Signature (a tiny library to protect against virtual DOM injection attacks) - to mark every command object with a Symbol, so it cannot be send over the network.

I must admit that I caught myself at least a couple of times thinking that if there's no SQL database or because I'm using some virtual DOM I'm not vulnerable to injection attacks. Oh how wrong I was!

From the author of this blog

  • howlong.app - a timesheet built for freelancers, not against them!