SQL injection - when a prepared statement is not enough...

Nanne Baars · October 22, 2017

There are situations when a prepared statement is not enough to protect yourself against an SQL injection, in this blog we will explore a case where more protection is required…

Introduction

An SQL injection attack consists of insertion or “injection” of a malicious data via the SQL query input from the client to the application. In our example project we have a small Spring Boot based blog application. This application exposes an endpoint to fetch blog articles based on the author:

@GetMapping("/author/{author}")
fun blogsByAuthorSqlInjection(@PathVariable author: String) = blogRepository.findByAuthor1(author)

Our (naive) implementation of the repository looks like:

@Repository
class BlogRepository(val entityManager: EntityManager) {

  fun findByAuthor1(name: String): Object = entityManager
    .createNativeQuery("select * from blogs b where b.author = '" + name + "'", BlogEntry::class.java)
    .resultList as Object
}

When we call the endpoint, we will receive:


curl -i http://localhost:8080/blogs/author/Anne%20Wilson

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 02 Oct 2017 20:19:50 GMT

[ {
  "title" : "Spring Boot with Kotlin",
  "publishDate" : "2017-09-28",
  "author" : "Anne Wilson",
  "contents" : "Spring 5.0 ..."
}, {
  "title" : "New Spring Boot version is available",
  "publishDate" : "2017-10-02",
  "author" : "Anne Wilson",
  "contents" : "A new version ..."
} ]

If the attacker however supplies the following:

curl -i "http://localhost:8080/blogs/author/Smith'%20or%20'1'='1"
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 02 Oct 2017 20:21:23 GMT

[ {
  "title" : "First blog",
  "publishDate" : "2017-09-27",
  "author" : "Peter Jackson",
  "contents" : "This is ..."
}, {
  "title" : "Spring Boot with Kotlin",
  "publishDate" : "2017-09-28",
  "author" : "Anne Wilson",
  "contents" : "Spring 5.0 ..."
}, {
  "title" : "A new Spring Boot version is available",
  "publishDate" : "2017-10-02",
  "author" : "Anne Wilson",
  "contents" : "Version 2 ..."
}, {
  "title" : "Spring 5 is on its way",
  "publishDate" : "2017-10-02",
  "author" : "Eric William",
  "contents" : "Spring 5 ..."
} ]

So due to the fact that the application is not escaping the quotes an attacker is able to modify the query and list all the blogs.

When you read about how to prevent SQL injections the most common advise is: “Use a prepared statement or parametrized queries.” If we change our repository method accordingly we will get:

fun findByAuthor(name: String): List<BlogEntry> =
  return entityManager.createQuery("select b from BlogEntry b where b.author = :name")
           .setParameter("name", name)
           .resultList as List<BlogEntry>

in which case the call made by the attacker will result in an empty JSON message because there is no author with the name Smith'%20or%20'1'='1 The attacker can no longer change the meaning of the query. ORM implementations like JPA/Hibernate will help you automatically to prevent SQL injections but as we will see in the next paragraph it is not enough.

Not enough

Although using prepared statements is a great step in the defense of preventing SQL injections there are cases where this is not enough, suppose we have the following query: “select * from blogentry order by author” The order by clause will normally be a column name, however if we look at the SQL grammar definition it can be:

SELECT ...
FROM tableList
[WHERE Expression]
[ORDER BY orderExpression [, ...]]

orderExpression:
{ columnNr | columnAlias | selectExpression }
    [ASC | DESC]

selectExpression:
{ Expression | COUNT(*) | {
    COUNT | MIN | MAX | SUM | AVG | SOME | EVERY |
    VAR_POP | VAR_SAMP | STDDEV_POP | STDDEV_SAMP
} ([ALL | DISTINCT][2]] Expression) } [[AS] label]

Looking at this we can see we can use functions inside the order by clause. Let’s change our example and add the following code to our controller and repository:

@GetMapping("/author/{author}")
fun blogsByAuthor(@PathVariable author: String, @RequestParam sortBy: String?) =
  blogRepository.findByAuthorContaining(author, sortBy)


fun findByAuthorContaining(name: String, orderBy: String): List<BlogEntry> =
  return entityManager.createQuery("select b from BlogEntry b where b.author :name order by " + orderBy)
            .setParameter("name", name)
            .resultList as List<BlogEntry>

As you can see the sortBy parameter is passed in through the REST endpoint. The prepared statement can only deal with query parameters (single value) and cannot be used with a column names, table names, expressions etc. This means the order by is just appended to the given query string.

One way to test whether this query is vulnerable for SQL injection is:

(select * from BlogEntry where b.author = :name order by case when true=true then title else contents)

curl -i "http://localhost:8080/blogs/author/Anne%20Wilson?sortBy=case%20when%20true=true%20then%20title%20else%20contents%20end"
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 02 Oct 2017 21:16:16 GMT

[ {
  "title" : "Spring Boot with Kotlin",
  "publishDate" : "2017-09-28",
  "author" : "Anne Wilson",
  "contents" : "Spring 5.0 ..."
}, {
  "title" : "A new Spring Boot version is available",
  "publishDate" : "2017-10-02",
  "author" : "Anne Wilson",
  "contents" : "Version 2 ..."
} ]

If we flip the when statement the result will be sorted based on the contents instead of the title. This means we can substitute random expressions for the sortBy query parameter, which means we can ask the database questions like:

select * from BlogEntry order by case when substring(h2version(),1,1)='1' then title else contents end
select * from BlogEntry order by case when substring(h2version(),2,1)='.' then title else contents end
select * from BlogEntry order by case when substring(h2version(),3,1)='4' then title else contents end
...

which will give you the database version(1.4.9) of H2 used in our example project. The order by clause is just an example it also applies group by clauses etc. You can also start asking system tables like INFORMATION_SCHEMA for other interesting tables in the database, sqlmap(http://sqlmap.org/) is a tool which can help you automate the extraction process.

Spring Data JPA

Our small example above was also present in an earlier version of Spring Data where you were able to specify a sort expression, like:

@Query("select p from Person p where LOWER(p.lastname) = LOWER(:lastname)")
List<Person> findByLastname(@Param("lastname") String lastname, Sort sort)

In the old version of Spring Data you could call this method as follows: findByLastname("Johnson", new Sort("LENGTH(firstname)")) which means you can repeat the same SQL injection as we described above. In the newer version of Spring Data an exception is thrown whenever you use a function, you still can use a function but you have to use: JpaSort.unsafe("LENGTH(lastname)" which clearly indicates a potential dangerous operation when the input can be adjusted by an attacker. More details can be found here: https://pivotal.io/security/cve-2016-6652

Mitigation

If you need to provide a sorting column in your web application you should implement a whitelist to validate the value of the order by statement it should always be limited to something like ‘firstname’ or ‘lastname’ or 1,2 etc. Remember it is a combination of both, you should use both always use a prepared statement when dealing with SQL but also use input validation on parts of the query which are not seen as a dynamic query parameter when using a prepared statement.

Twitter, LinkedIn