Big Bird: killing SQL injection with graph homomorphisms
dustri.org
dustri.org
While attending various security vendor demos of static analysis products, they all used SQL injection as their example, and all of them recommended input validation as the mitigation rather than parameterised queries. You'd think they would know better.
It's a cute idea to compare the AST of the query to see if it changes, but then you have to define all valid queries - a whitelisting approach. I'm struggling to see why you would want it unless you had control over a database but had no control over the code that issues the queries.
Parametrisation is the Right Thing™ and yet people love inventing new half-arsed workarounds when they could just use parameterisation.
Elsewhere in security, historically you would find any number of hacks to try to avoid needing to use a good Mode Of Operation for your block cipher. People would rather expend mental effort and protocol complexity on trying to still do ECB when that's the Wrong Thing™ than just use the Right Thing™ (these days that's an inherently Authenticated Encryption Mode of Operation like GCM or GCM-SIV)
Or look at all the work trying to shore up human memorable shared secrets (passwords) when you almost never actually needed this and could have instead deployed stuff like WebAuthn.
You still need to build the queries and have control of what parametres are in use. Why not build the parameters list dynamically as well?. There are some databases or drivers that have limitations where you need to sanitize data yourself, but I'm not sure if that applies to the majority of the scenarios.
This is a simple example of code for constructing dynamic queries:
query = "... WHERE country = :country";
params.set("country", country);
if (userId != null) {
query += " AND user_id = :userId";
params.set("userId", userId);
}
It may become more complex as you add sub queries or if the database does not support adding a list as a parameter, but the principle is the same. You can always just build the list programatically using parameters instead of concatenating the values given by the user directly.If there are drivers or databases that does not support this, you will need to sanitize the data yourself of course. But I don't think that is the case for most users.
(btw, database drivers that support named parameters are much more convenient than using "?")
For example engines don’t always support a list parameter, meaning an “in” clause with varying number of entries can’t work. (Some might allow table variable parameters as a workaround, but not all).
Another common reason for needing dynamic query construction varying sort by. Once again many databases don’t let you do “order by ? ?” (First being column name, second ASC/DESC).
But a good query builder abstraction with knowledge of your specific DBMS, can help avoid injection here, especially if it does a sanity check of reparsing the generated query and comparing it against an AST it built of the target query. Even when using such a system though, it can make sense to have the query builder build a parameterized query, and use that for passing as many parameters as possible, just as defense in depth. (Avoids bugs in escaping code of query builder, for example.)
Now of course, if you’re trying to be super strict about parameterization so that static tools / linters / code policy during review don’t complain that’s one thing. But it’s not an invalid choice to utilize domain knowledge to simplify the problem if your tooling doesn’t let you do the right thing.
If you can prove that a user input is a harmless value (a valid column name for the table for an order by, an integer, etc) then concatenation is safe. On the other hand, it is still not ideal have a bunch of code where you are directly concatenating strings to generate SQL, since people making later changes are likely to end up modifying code to concatenate unsafe input.
Hence why having a good query builder api can be valuable. It can hide away the string concatenation, and if well designed, can force that a value but in the order by position is a column name, etc, ideally while making it hard or impossible to have insufficiently constrained user input thrown in.
For example, if a language supports distinguishing string literals (or compile time constant strings), and variable string values, it is possible to create a query builder api where arbitrary user provided strings simply cannot become part of the query. (e.g. The only paths that let non-constant strings into the query are those that highly constrain the strings to provably safe values).
Oh but PDO doesn't support params in "limit" statement. Than make it support it! Improve PDO's support for "in" parameters as well so that I can pass damn array.
Hack add a "strict mode" to PDO that makes any kind of value in SQL invalid statement.
Running some magic AI than randomly blocks valid SQL is not a solution.
Then I learned about parameterization, and had my suspicions justified but I didn’t understand why it wasn’t the standard answer.
Finally it turned out PHP mysql lib apparently didn’t support it for ages, and thus young minds have been poisoned by the ridiculous notion of string escaping for decades.
It's still a thing even today with shit like html escaping; I don’t understand why we’re so explicit on so many things in programming, and then as soon as we try talking to another system we just drop everything back to stringly typed nonsense
Keep the inputs out-of-band and this whole problem doesn’t even exist as a concept
And at the same time everyone and their grandmother was making websites using PHP and MySQL.
There should have been enough incentive to improve this early on.
Killing SQL injection is not a hard problem. Parsing this statement, however, is.
To be fair, most web-browsers today supports Content Security Policy[1], where you can explicitly declare that NO, nothing inside this HTML is ever allowed to be executed as code. The only code allowed comes from script tags, from these domains, which you may optionally require to match a specific hash.
It's not that hard.
Ofcourse the application has to be built to deliver the front-end payload that way for this to work, but it's not exactly rocket-science.
That can easily be avoided by producing a different type of thing to the input, e.g.
escapeHTML : String -> HTMLChunk
escapeSQL : String -> SQL
escapeShell : String -> Shell
etc.
and having our "execute" API requires that thing as input, e.g. returnResponse : HTML -> ()
runQuery : SQL -> ResultSet
runCmd : Shell -> (ExitCode, Stdout, Stderr)
This way, we're forced to escape (to get a valid argument for the API), and we can't double-escape (since escaping functions require a string as input)One thing I thought of just now was the unlimited power of the database connection. When a programmer sets up a connection, it is usually for fairly simple things. And then injection happens and the connection happily processes that as well.
Perhaps it would be nice to allow the programmer to define the limited scope when opening the connection. A parameter could be added to specify a list of tables and the expected operations. So the programmer could say that the connection is going to access PRODUCTS and the only expected operation will be SELECT.
Closing the connection could also be modified to return the set of tables accessed during the connection along with the operations observed. Another thing that could be returned is a set of AST's detected with the AST in string form. These AST's could then be incorporated into the source code and also passed to the database when the connection is opened.
The database could then reject SQL that attempts to access tables outside of the specified set of allowed tables and operations. Likewise, unexpected queries could be failed.
The beauty of this is that the management of the whitelisted items is in the domain and control of the programmer. Not specifying whitelisted items is possible still and gives the status quo if that is what is wanted. It also empowers and rewards the programmer would really, really does not want to be responsible if things go crazy.
Little Bobby Tables deserves to have his name accurately represented in the database. This could involve proper escaping (difficult) or parameterized queries/prepared statements (easy). But many sqli mitigations just reject his name out of hand.
I will say that sometimes I'm a little thankful for SQL injection, or at least a little bit of leeway for abuse. Many applications implement partial search using "like '${str}%'" and if they don't escape % I can also search for my string in the middle. There have been times when I'm able to inject additional columns or rename things to exfiltrate data that should have been available but the app developer didn't make it accessible. Unfortunately, in the wrong hands it is able to break things. Even accidentally.
The only way I could see to avoid an embedded string altering syntactic meaning is if the syntax either gives the length of the parameter before it, or surrounds the parameter with some unguessable values. For example:
SELECT "10:xxxxxxxxxx" FROM USERS; SELECT "s63hfe&:xxxxxxxxxxs63hfe&" FROM USERS;
Where xxxxxxxxxx is the user supplied value.
Both of these are totally impractical of course.
#!/usr/bin/env perl
use 5.020_000;
use warnings;
use DBI qw<>;
my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:');
my $sth = $dbh->prepare('SELECT ("quux-" || ?) AS foo');
$sth->execute('bar');
say $sth->fetchrow_hashref->{foo};
... gives: $ perl c.pl
quux-bar