Thanks for your feedback, Anthony.
I'll take a few of your points in turn.
With regards to the fact that not all SQL queries are directly
parameterizable, this is true. Structural parts of a query, such as table
names, column names and complex conditions are hard to parameterize with
"vanilla" prepared statements, and many developers like to abstract some of
these structural parts of a SQL query into config files, and append
additional conditional constraints into the query at runtime based on user
input.
This feature addresses this head on. So long as the structural parts of the
prepared statement -- that is, table names, database names and column names
-- are not themselves attacker-controlled (I can't think of a valid reason
whey they would be), this feature is happy for developers to concatenate
them into a query string. For example, the following is not detected by the
feature as dangerous, because the query (whilst dynamically constructed) is
the linear concatenation of string literals, and so is a safeconst.
$query = "SELECT * from {$config['tablename']} WHERE id=?";
if(isset($_GET["filterbycolor"]))
$query .= " AND color=?";
do_prepared_statement($query, array("id" => $_GET["id"] "color" =>
$_GET["color"]));
We can even take this to an extreme to prove how flexible this can be.
Here's a function that issues a SQL statement where everything from the
table name and column name, to the structure and form of the condition, as
well as the limit, order and direction of the output are all chosen by the
caller. So long as the structural parts (i.e. the table names, database
names and column names) are safeconsts, the resulting query is a safeconst,
and this feature correctly identifies the resulting query as untainted).
http://phpoops.cloudapp.net/dynamic_pdo.php?viewcode
http://phpoops.cloudapp.net/dynamic_pdo.php
Finally, this feature is off-by-default. This means that that the feature
only helps developers who want to (or are required to) parameterize their
queries. It detects queries where the query cannot be proven "safe", and
then informs the developer that they need to upgrade the query so that the
dynamic parts of the query are parameterized to be in compliance with the
website's PHP.INI settings for this feature.
==
With regard to static analysis, I have a lot of respect for static analysis
tools, but especially in the webapplication world, they lack adoption, and
hence have limited real-world value. In over a decade of security
consulting experience, and with the notable exception of Google and
Microsoft, I've never seen any company that routinely scanned their website
with static analysis tools more complex than "grep".
As well as poor adoption, static analysis tools quickly run into
intractible problems doing analysis over unbound-loops, indirect function
calls or eval statements, and can get confused when looking at files out of
context (e.g. scanning included files). Runtime analysis, on the other
hand, always runs, has zero startup cost, negligble runtime cost and
guarrantees to check every codepath that is actually taken; regardless of
how it was reached.
For this reason, I think static analysis is the wrong tool for this job.
==
With regard to SQL parameterization not always being safe, the case you are
citing is due to a security defect in the way PHP emulates SQL
parameterization for old versions of MySQL that don't support
parameterization. In this particular case, PHP "emulates" the
parametization by escaping the parameters, and -- since escaping is a
dangerous way to build SQL query statements -- this goes horribly wrong and
leads to a SQL injection. But to be clear: this is a defect in PHP's
/emulation/ of prepared statements when real prepared statements are not
available, not a problem of prepared statements themselves.
Where parameterized queries are available at the database layer, and PHP is
not defectively emulating them, they are (by definition) immune from SQL
injection. Parameterized queries are not flattened to a string using
escaping. They are sent to the database in parameterized form, and are
replaced at AST-construction stage in the database itself. This means they
are immune from weird bugs like character-set differences between
application and SQL-server: so long as the query is untainted, the attacker
cannot affect the structure of the AST using only parameter values, and
hence can never SQL-inject the database.
To quote Wikipedia's article on prepared statements (
https://en.wikipedia.org/wiki/Prepared_statement):
>> Prepared statements are resilient against SQL injection, because
parameter values, which are transmitted later
>> using a different protocol, need not be correctly escaped. If the
original statement template is not derived
>> from external input, SQL injection cannot occur.
To quote the W3C (http://www.w3schools.com/sql/sql_injection.asp)
>> The only proven way to protect a web site from SQL injection attacks, is
to use SQL parameters.
To quote security consortium OWASP (
https://www.owasp.org/index.php/Query_Parameterization_Cheat_Sheet)
>> SQL injection is best prevented through the use of parameterized queries.
And to quote one of Microsoft's security engineers on the SQL team,
specifically addressing PHP (
http://blogs.msdn.com/b/brian_swan/archive/2010/03/04/what_2700_s-the-right-way-to-avoid-sql-injection-in-php-scripts_3f00_.aspx
):
*>> The most common suggestion I’ve seen for preventing SQL injection
involves trying to remove or escape any possible SQL code from user input
before concatenating it with the SQL code to be executed. There are several
PHP functions (and functions in PHP extensions) that can be used to do
this, but all of them are potentially vulnerable. If you concatenate user
input with SQL code that will be executed in the database, you run the risk
of a SQL injection attack no matter how much parsing and escaping of the
input you do. How can you be 100% sure that you’ve thought of all
possibilities that a creative hacker might think of? How can you be sure
that you’ve taken the appropriate measures to mitigate an attack? How can
you be sure that the functions you are using to remove or escape dangerous
user input aren’t buggy?*
*>> The right way to prevent SQL injection is by using parameterized
queries. This means defining the SQL code that is to be executed with
placeholders for parameter values, programmatically adding the parameter
values, then executing the query. Doing this allows the server to create an
execution plan for the query, which prevents any "injected" SQL from being
executed. An example will help in explaining this. Let’s use the same
script, but I’ll define the SQL query with parameter placeholders.*
You are of course welcome to disagree with the overwhelming body of
security advice that parameterized queries are the correct, secure way to
prevent SQL injection. In that case, you only need to not enable this
feature. This feature is off-by-default, and only attempts to help secure
webapplications and webdevelopers who do (or are required, for example by
PCI compliance standards) to adopt this security-best-practice to ensure
that they do so systematically across their entire website.
==
With regard to SQL extensions that don't ship with PHP, it is true that
this doesn't add security to builtin functions that don't ship with PHP or
its various extensions. Extensions that ship entirely independently to PHP
are free to copy the two-line edit to their codebase to also take advantage
of the feature. If they choose not to, that is their prerogative. In the
meantime, securing MySQLi, Postgres and PDO seems to me to be a good place
to start.
==
With regard to training, I 100% agree that developer security training in
general is pretty bad, but especially so for PHP. For example, none of the
pages on SQL builtins (like this one
http://php.net/manual/en/function.mysql-query.php) mention SQL injection,
why it is bad, and how to prevent it.
By way of contrast, see Microsoft's Database.SqlQuery documentation (
https://msdn.microsoft.com/en-us/library/system.data.entity.database.sqlquery(v=vs.113).aspx)
which states:
>> Creates a raw SQL query that will return elements of the given type. The
type can be any type that has properties that match the names of the
columns returned from the query, or can be a simple primitive type. The
type does not have to be an entity type. The results of this query are
never tracked by the context even if the type of object returned is an
entity type. Use the SqlQuery method to return entities that are tracked by
the context. *As with any API that accepts SQL it is important to
parameterize any user input to protect against a SQL injection attack. You
can include parameter place holders in the SQL query string and then supply
parameter values as additional arguments*. Any parameter values you supply
will automatically be converted to a DbParameter.
context.Database.SqlQuery(typeof(Post), "SELECT * FROM dbo.Posts WHERE
Author = @p0", userSuppliedAuthor); Alternatively, you can also construct a
DbParameter and supply it to SqlQuery. This allows you to use named
parameters in the SQL query string. context.Database.SqlQuery(typeof(Post),
"SELECT * FROM dbo.Posts WHERE Author = @author", new
SqlParameter("@author", userSuppliedAuthor)); (emphasis added).
This feature helps address some of this technical debt. This feature gives
website owners (who may be nontechnical, or technical but with limited
oversight of their large workforce) a tool to prevent unsafe SQL-queries
being written in the first place. If one of their developers then writes
(or exposes) a query that could be tainted with attacker data -- even if
the user data isn't an injection attack in progress -- PHP blocks the query
and provides the developer with a link that can tell them about SQL
injection, why their query was blocked and how they can restructure their
query so that it's not vulnerable to SQL injection any more. By doing this
/as the developer writes code/, rather than via some static analysis tool
or security audit weeks or months later, developers will quickly discover
that writing parameterized queries is the only way to dynamically interact
with a database when the feature is enabled.
Hope that clears some of this up,
Matt
On 5 August 2015 at 21:53, Anthony Ferrara <ircmaxell@gmail.com> wrote: