SQL injection: complete
             walkthrough (not only) for PHP
             developers


                      Krzysztof Kotowicz
                      PHP Developer

                      http://web.eskot.pl
OWASP                 Medycyna Praktyczna
                      krzysztof@kotowicz.net
10.03.2010

                 Copyright © The OWASP Foundation
                 Permission is granted to copy, distribute and/or modify this document
                 under the terms of the OWASP License.




                 The OWASP Foundation
                 http://www.owasp.org
Plan

   What is SQL injection?
   Why is it so dangerous (demo)?
   How to defend?
     • Prepared statements
     • Escaping
     • Stored procedures
     • Additional methods
   Summary



                                     OWASP   2
Discussed databases (RDBMS)

   MySQL
   Oracle
   MS SQL Server
   To some extent:
      • PostgreSQL
      • SQLite




                              OWASP   3
Discussed PHP projects
    PDO – PHP data objects
    • Common interface for various RDBMS
    Doctrine 1.2
    • ORM (Object Relational Mapper) used e.g. in Symfony framework
    Propel 1.4
    • ORM, like Doctrine
    • Used in Symfony
    Zend Framework 1.10
    • Popular framework MVC for PHP
    MDB2 2.4.1
    • Database abstraction layer (DBAL)
    • Distributed through PEAR

                                                              OWASP   4
What is SQL injection?




                         OWASP   5
SQL injection – short definition
It is a kind of web application attack, where user-
supplied input coming from:

  URL: www.example.com?id=1
  Forms: email=a@example.com
  Other elements: e.g. cookie, HTTP headers


is manipulated so that vulnerable application
executes SQL commands injected by attacker.



                                               OWASP   6
Example – login form
SELECT * FROM users WHERE login = '{$login}' and
password_hash = MD5('{$password}')

$login = "' or 1=1 -- ";
            "anything";
$password = "dowolne";

// zamierzalismy osiagnacthis(kod  dane)
   you wanted to achieve to (code  data)
SELECT * FROM users WHERE login = '' or 1=1 -- '
and password_hash = MD5('dowolne')
                    MD5('anything')

// but server interprets it as
SELECT * FROM users WHERE login = '' or 1=1 -- '
and password_hash = MD5('anything')


User logs in without knowing the login nor password

                                            OWASP      7
Why is it so dangerous?
       DEMO




                          OWASP   8
What are the possible threats?

 Unauthorized access to application
 Access to whole database / databases on
  the server
 Denial of service
 Database modification
 Read / write files on server's filesystem
 Code execution



                                      OWASP   9
A few facts

    Injection vulnerabilities are the 1st on OWASP Top
     10 2010 RC
    SQLi is responsible for 40–60% cases of data
     breach [1] [2]
    Modern attack techniques are advanced and
     automated
    • Vulnerability is not only in WHERE part
    • Sometimes it is enough to break a query
    Vulnerabilities are found on a daily basis, even in
     new applications


                                                OWASP      10
How to defend?




                 OWASP   11
How to defend against SQL injection?

 Source of vulnerability is mixing code with data
 SELECT * FROM users WHERE login = 'login'


Defense methods
  Separating code from data
    prepared statements
    stored procedures
  Escaping



                                             OWASP   12
How to defend?
Prepared statements




                      OWASP   13
Prepared statements – how to use?

1.   Preparing SQL command (string)
     Put placeholders where data should be
                         WHERE a = ? ... WHERE a = :col

2.   Send command to server                    PREPARE
3.   Attach data to command
4.   Execute command                           EXECUTE
5.   Fetch results

     3, 4, 5 could be repeated...
6.   Clear the command

                                               OWASP      14
Prepared statements - example

 PDO
// prepare command
$stmt = $dbh->prepare("INSERT INTO SUMMARIES
  (name, sum) VALUES (:name, :sum)");

// attach data variables - WITH ITS TYPES!
$stmt->bindParam(':name', $name, PDO::PARAM_STR);
$stmt->bindParam(':sum', $sum, PDO::PARAM_INT);

// bind values
$name = 'something';
$value = 1234;

// execute command
$stmt->execute();
$stmt = null; //free memory
                                           OWASP    15
Prepared statements - advantages

   Commands are completely separated from data they
    operate on
   Injection is not possible
   Command is compiled only once - potential speedup



$stmt->bindParam(':name', $name, PDO::PARAM_STR);
$stmt->bindParam(':sum', $sum, PDO::PARAM_INT);

// petla po danych...
foreach ($do_bazy as $name => $value) {
  $stmt->execute();
}

                                              OWASP     16
Prepared statements - caveats

   Not all commands may be parametrised
   You cannot put parameters everywhere

    -- error
    SELECT * FROM :table
    SELECT :function(:column) FROM :view

    -- not what you expect
    SELECT * FROM table WHERE :column = 1
    SELECT * FROM table GROUP BY :column


   Just using PS does not enforce using parameters in
    them
   Sometimes they're emulated (it's a good thing!)
                                                OWASP    17
Prepared statements in Doctrine

   Uses PDO   (emulated for Oracle)   and prepared statements
   Uses own DQL language instead of SQL

$q = Doctrine_Query::create()
  ->select('u.id')
  ->from('User u')
  ->where('u.login = ?', ‘mylogin');

echo $q->getSqlQuery();
// SELECT u.id AS u__id FROM user u
// WHERE (u.login = ?)

$users = $q->execute();

                                                       OWASP     18
Prepared statements in Doctrine cont.

 It can still bite you
    $q = Doctrine_Query::create()
        ->update('Account')
        ->set('amount', 'amount + 200')
        ->where("id > {$_GET['id']}");

 Correct this to:

    ->where("id > ?", (int) $_GET['id']);


 NEVER put input data directly into SQL
  commands

                                            OWASP   19
Prepared statements in Propel

 Uses PDO, like Doctrine
// through Criteria
$c = new Criteria();
$c->add(AuthorPeer::FIRST_NAME, "Karl");
$authors = AuthorPeer::doSelect($c);

// through custom SQL (sometimes it's more convenient)
$pdo = Propel::getConnection(BookPeer::DATABASE_NAME);
$sql = "SELECT * FROM complicated_sql
        JOIN some_big_join USING something
        WHERE column = :col)”;
$stmt = $pdo->prepare($sql);
$stmt->execute(array('col' => 'Bye bye SQLi!');


                                            OWASP    20
Prepared statements in Zend Framework

   PDO (+ mysqli + oci8 + sqlsrv)

// prepare + execute
$stmt = $db->prepare('INSERT INTO server (key,
          value) VALUES (:key,:value)');
$stmt->bindParam('key', $k);
$stmt->bindParam('value', $v);

foreach ($_SERVER as $k => $v)
   $stmt->execute();

// prepare + execute in one step
$stmt = $db->query('SELECT * FROM bugs WHERE
      reported_by = ? AND bug_status = ?',
       array('goofy', 'FIXED'));

while ($row = $stmt->fetch())
    echo $row['bug_description'];
                                                 OWASP   21
Prepared statements in MDB2

 Based on different database drivers (mysql,
  oci8, mssql, ...)
 Emulates PS, if database doesn't support them
    $types = array('integer', 'text', 'text');
    $stmt = $mdb2->prepare('INSERT INTO numbers
    VALUES (:id, :name, :lang)', $types);

    $data = array('id' => 1,
                  'name' => 'one',
                  'lang' => 'en');

    $affectedRows = $stmt->execute($data);
    $stmt->free();

                                             OWASP   22
Prepared statements - summary

 They offer very good protection (if used
  properly)
 Easy to use, small changes in code
 Good support in frameworks
 They have their limits
 Sometimes they have to be used with other
  defense methods




                                      OWASP   23
How to defend?
Escaping data




                 OWASP   24
Escaping – how does it work?

    Data and commands are still kept in a single variable, but
     we try to separate them inline
    Numbers
    • Cast to (int) / (float) – don't use is_numeric [1]!
    Texts are surrounded with single quotes : '

      .. WHERE col = 'TEXT DATA' AND ...

    • If quote is inside the text, you need a way to distinguish it from
      the ending quote
    • Prepend a special character e.g. "" to a quote
    • Escaping rules depend on context!


                                                             OWASP         25
Escaping – context

addslashes()
   Returns a string with backslashes before characters that need to be quoted in
   database queries etc. These characters are single quote ('), double quote ("),
   backslash () and NUL (the NULL byte).
   / Source: php.net manual /


$user = addslashes($_GET['u']);
$pass = addslashes($_GET['p']);

$sql = "SELECT * FROM users WHERE username =
        '{$user}' AND password = '{$pass}'";

$ret = exec_sql($sql);


 Are you safe?

                                                                       OWASP        26
NO

     OWASP
Escaping – context cont.

    Different RDBMS have different ways of escaping data
     (it also depends on configuration)
    addslashes() works just like MySQL only „by chance”
    RBDMS                PHP function        i've got quotes
     PDO         $pdo->quote($val, $type)      n/a (it depends)
MySQL (mysql)    mysql_real_escape_string    i've got quotes
MySQL (mysqli)   mysqli_real_escape_string   i've got quotes
 Oracle (oci8)      n/d - str_replace()      i''ve got quotes
    SQLite         sqlite_escape_string      i''ve got quotes
MS SQL (mssql)      n/d - str_replace()      i''ve got quotes
 PostgreSQL         pg_escape_string()       i''ve got quotes

                                                       OWASP      28
Escaping – context cont.
// SELECT * FROM users WHERE username =
// '{$user}' AND password = '{$pass}'
$_GET['u'] = "anything'";
$_GET['p'] = " or 1=1 -- ";

// MySQL sees it as :
SELECT * FROM users WHERE username = 'anything''
AND password = ' or 1=1 -- '

// SQLite / MS SQL / Oracle / PostgreSQL:
SELECT * FROM users WHERE username = 'anything''
AND password = ' or 1=1 -- '



   Don't use addslashes(), use PHP functions for your
    RBDMS
   Are you safe now?
                                             OWASP       29
ALMOST

         OWASP
Escaping gotchas – charsets

 Errors discovered in 2006 in PostgreSQL and
  MySQL [1] [2]
 In some multibyte charsets despite escaping you
  can cause SQL injection
  is „swallowed” by multibyte character
 Example:
     •   BF 27 [ ¬ ' ]  BF 5C 27 [ ¬  ' ]
     •   First 2 bytes are character ¿ in GBK charset
     •   Server will see   ¿'

                                                    OWASP   31
Escaping gotchas – charsets

 Some Asian charsets are vulnerable
 Luckily - not UTF-8!
 In PostgreSQL '' escaping was used (instead
  of ')
 In mysql_real_escape_string()
  escaping is done with respect to current
  connection charset
     •   Doesn't always work! [1] [2]
 Charset also defines context

                                        OWASP   32
Escaping gotchas – object names

 Colum, table, database etc. names
       •   No common good rule to escape them
       •   Different reserved words, different maximum
           name lengths etc.

If you need to get those names from the user - use
     whitelisting (blacklisting if you really can't do
     otherwise)




                                                    OWASP   33
Escaping gotchas – object names cont.

 Example - sorting by column
 There's a vuln. in $order, but you can't
  escape there
$cat_id = (int) $_GET['cid'];
$order = $_GET['column'];
$stmt = $pdo->prepare("SELECT * FROM products WHERE
                       cid = :cid ORDER BY $order");

$stmt->bindParam(':cid', $cat_id, PDO::PARAM_INT);

if ($stmt->execute()) {
    ...
}

                                            OWASP      34
Escaping gotchas – object names cont.

 Whitelisting
  $columns = array( // list of allowed columns
  'product_name','cid','price',
  );

  if (!in_array($order, $columns, true))
  $order = 'product_name'; // default column

 Blacklisting
  // only a-z and _
  $order = preg_replace('/[^a-z_]/', '', $order);

  // max 40 characters
  $order = substr($order, 0, 40);

                                                 OWASP   35
Escaping in PDO

 PDO::quote($value, $type, $len)
 Length and type are sometimes ignored!
     •   Cast numbers to (int), (float)
     •   Texts – cut them manually
$quoted = $pdo->quote($input, PDO::PARAM_STR, 40);




                                           OWASP     36
Escaping in Doctrine

 Careful with Doctrine quote()!
$q = Doctrine_Query::create();
// not like this!!!
$quoted = $q->getConnection()->quote($input, 'text');

$q->update('User')->set('username', $quoted);
// quote() only changes ' to '' - exploit (MySQL):
$input = 'anything' where 1=1 -- ';

// escape through PDO - getDbh():
$quoted = $q->getConnection()
            ->getDbh()
            ->quote($input, PDO::PARAM_STR);
// 'anything ' where 1=1 -- '


                                               OWASP    37
Escaping in Propel

 Through PDO::quote()


$pdo = Propel::getConnection(UserPeer::DATABASE_NAME);

$c = new Criteria();
$c->add(UserPeer::PASSWORD,
        "MD5(".UserPeer::PASSWORD.") "
        ." = " . $pdo->quote($password),
        Criteria::CUSTOM);




                                              OWASP      38
Escaping in Zend Framework

 Functions quote(), quoteInto()


$name = $db->quote("O'Reilly");
// 'O'Reilly'

// simplified escaping for a single value
$sql = $db->quoteInto("SELECT * FROM products WHERE
 product_name = ?", 'any string');




                                            OWASP     39
Escaping in MDB

 quote()

// quote() function - give type
$query = 'INSERT INTO table (id, itemname,
          saved_time) VALUES ('
 . $mdb2->quote($id, 'integer') .', '
 . $mdb2->quote($name, 'text') .', '
 . $mdb2->quote($time, 'timestamp') .')';

$res = $mdb2->exec($query);




                                             OWASP   40
Escaping - summary

   Looks easy - search and replace
   Just looks
     •   You need to know the context (database, charset)
     •   There are invalid implementations
   Encourages invalid practices
     •   concatenating strings to form a SQL command
     •   ignoring numeric parameters
   Use only if
     •   You program for a single RDBMS
     •   There is no other way



                                                       OWASP   41
How to defend?
Stored procedures




                    OWASP   42
Stored procedures

   SQL command(s) is moved to database server and
    stored there under a name
   Client executes a procedure with input and output
    parameters
   In output parameters client receives results

   Data is formally separated from code
   It's NOT enough




                                                OWASP   43
Stored procedures cont.

 Example for MS SQL – a vulnerable procedure

CREATE PROCEDURE SP_ProductSearch
@prodname varchar(400)
AS
 DECLARE @sql nvarchar(4000)
 SELECT @sql = 'SELECT ProductID, ProductName,
    Category, Price FROM Product Where ProductName
    LIKE ''' + @prodname + ''''
 EXEC (@sql)
 ...

 It's just like eval()!
                                           OWASP     44
Stored procedures cont.

 Same vulnerability in Oracle

CREATE OR REPLACE PROCEDURE
SP_ProductSearch(Prodname IN VARCHAR2) AS
       sqltext VARCHAR2(80);
BEGIN
   sqltext := 'SELECT ProductID, ProductName,
            Category, Price FROM Product
           WHERE ProductName LIKE '''
            || Prodname || '''';
   EXECUTE IMMEDIATE sqltext;
   ...
END;


                                           OWASP   45
Stored procedures – Dynamic SQL

 Vulnerability lies in Dynamic SQL
 • Data is again mixed with code in one variable
 How to defend?
 • Separate the code from data
 • Escape




                                         OWASP     46
Stored procedures in MS SQL

 Separating code from data
 • use sp_executesql with parameter list

CREATE PROCEDURE SP_ProductSearch @prodname
varchar(400) = NULL AS
DECLARE @sql nvarchar(4000)
SELECT @sql = N'SELECT ProductID, ProductName,
               Category, Price FROM Product Where
               ProductName LIKE @p'
EXEC sp_executesql @sql,
     N'@p varchar(400)',
     @prodname


                                            OWASP   47
Stored procedures in MS SQL cont.

 Escaping character data
   Object name         QUOTENAME(@v)

   Text <= 128 chars   QUOTENAME(@v,'''')

   Text > 128 chars    REPLACE(@v,'''','''''')



 Example:
   SET @cmd = N'select * from authors where lname=''' +
   REPLACE(@lname, '''', '''''') + N''''

 Escape only when you must!
  (use sp_executesql with parameters)
                                                 OWASP    48
Stored procedures in Oracle

 Oracle - use EXECUTE IMMEDIATE ..
  USING
 CREATE OR REPLACE PROCEDURE
 SP_ProductSearch(Prodname IN VARCHAR2) AS
        sqltext VARCHAR2(80);
 BEGIN
   sqltext := 'SELECT ProductID, ProductName,
            Category, Price WHERE
            ProductName=:p';
   EXECUTE IMMEDIATE sqltext USING Prodname;
   ...
 END;

 Escaping - DBMS_ASSERT package

                                           OWASP   49
Stored procedures in MySQL

 Support for Dynamic SQL only through
  prepared statements
 It's actually harder to make vulnerable
  procedure
 Just use placeholders




                                        OWASP   50
Stored procedures in MySQL cont.

 PREPARE / EXECUTE USING /
  DEALLOCATE PREPARE
DELIMITER $$
CREATE PROCEDURE get_users_like (
  IN contains VARCHAR(40))
BEGIN
  SET @like = CONCAT("%", contains, "%");
  SET @sql = "SELECT * FROM users WHERE uname LIKE ?";
  PREPARE get_users_stmt from @sql;
  EXECUTE get_users_stmt USING @like;
  DEALLOCATE PREPARE get_users_stmt;
END$$
DELIMITER ;


                                             OWASP       51
Stored procedures in MySQL cont.

 Or, even simpler


DELIMITER $$
CREATE PROCEDURE get_users_like (
  IN contains VARCHAR(40))
BEGIN
  SET @like = CONCAT("%", contains, "%");
  SELECT * FROM users WHERE uname LIKE @like;
END$$
DELIMITER ;



 Escaping – QUOTE() function
                                                OWASP   52
Stored procedures in PHP

   Different support level, depending on RDBMS
   Common API (e.g. PDO) only for simple calls
     •    No return from procedure
     •    Returns scalar value in OUT parameter
   Different API (or none at all) for advanced calls
     •    e.g. cursors, fetching records sets
   Almost no support in frameworks
   Still some errors...




                                                   OWASP   53
Stored procedures in PDO

 Calling a procedure

// MySQL
$sql = "CALL get_users_like(:contains)";
// MS SQL – EXEC get_users_like :contains

$stmt = $pdo->prepare($sql);
$ret = $stmt->execute(array('contains' => $input));

foreach($stmt->fetchAll() as $users) {
  var_dump($users);
}

unset($s);



                                            OWASP     54
Stored procedures in
Doctrine/Propel/Zend Framework
 Doctrine - no support (use PDO)
$pdo = Doctrine_Manager::connection()->getDbh();


 Propel – likewise
$pdo = Propel::getConnection(UserPeer::DATABASE_NAME);


 Zend Framework – likewise

$pdo = $db::getConnection();



                                               OWASP     55
Stored procedures in MDB2

 You need to manually escape all parameters!

 $mdb2->loadModule('Function');
 $multi_query = $mdb2->setOption('multi_query', true);

 if (!PEAR::isError($multi_query)) {

        $result = $mdb2->executeStoredProc('get_users_like',
                  array($mdb2->quote($contains, 'text')));
 do {
     while ($row = $result->fetchRow()) {
           var_dump($row);
     }
 } while ($result->nextResult());
 }


                                                   OWASP       56
Stored procedures - gotchas

 Data length
CREATE PROCEDURE change_password
@loginname varchar(50),
@old varchar(50),
@new varchar(50)
AS
  DECLARE @command varchar(120)
  SET @command= 'UPDATE users SET password=' +
                 QUOTENAME(@new, '''') +
                 ' WHERE loginname=' +
                 QUOTENAME(@loginname, '''') +
                 ' AND password=' +
                  QUOTENAME(@old, '''')
  EXEC (@command)
GO

                                            OWASP   57
Stored procedures - summary
   Moving SQL logic to server takes time
   Code is not easily ported to other RDBMS
   You need to use prepared statements or escaping to
    write safe stored procedures anyway
   If done poorly, you're even more vulnerable
      • Both SP code and statement calling SP could be
          vulnerable
      • SP usually has greater permissions than code
          calling it
   Bad support in PHP and frameworks



                                             OWASP       58
Stored procedures - summary
SPs have many advantages outside our scope
    Could be used with different clients (Java/.NET + PHP)
    Could have better berformance
    and many more...
Conclusion:
You can write secure stored procedures, but they usually
    increase the application cost considerably
It is vital to write stored procedures protected against
      SQL injection




                                                     OWASP    59
How to defend?
Additional methods




                     OWASP   60
Validation and filtering
   Validate all external data
   Validate before processing
   Filter INPUT - escape OUTPUT
   Different validation rules for each parameter -
    check e.g.
     •   Type
     •   Scalar / array
     •   Min / max values
     •   Character data length! [1]



                                            OWASP     61
Additional methods

Complementary to all previously mentioned!
 Principle of least privilege when connecting to DB
 Removing unused functions, accounts, packages
   shipped with database
 Routinely updating the system and database software
 Correct PHP and database configuration
      •   magic_quotes_* = false
      •   display_errors = false
   Good database design



                                             OWASP      62
Summary

   Pay attention to SQL injection - even a single mistake
    could cost you!
   Prefer complete solutions - e.g. frameworks
   Filter and validate all input data
   Remeber about data types and lengths
   Prefer whitelisting to blacklisting - the latter will fail
    one day!
   Use prepared statements whenever you can
   Try to avoid escaping
   In stored procedures double check your Dynamic SQL


                                                    OWASP        63
Links
       Discussed projects
    •      sqlmap.sourceforge.net
    •      php.net/manual/en/book.pdo.php
    •      www.doctrine-project.org
    •      propel.phpdb.org/trac
    •      framework.zend.com
    •      pear.php.net/package/MDB2
       About SQL injection
    •      www.owasp.org/index.php/SQL_Injection
    •      unixwiz.net/techtips/sql-injection.html
    •      delicious.com/koto/sql+injection
       Hack me
    •      threats.pl/bezpieczenstwo-aplikacji-internetowych
    •      tinyurl.com/webgoat
    •      mavensecurity.com/dojo.php
       krzysztof@kotowicz.net       http://blog.kotowicz.net
                                                                OWASP   64

SQL Injection: complete walkthrough (not only) for PHP developers

  • 1.
    SQL injection: complete walkthrough (not only) for PHP developers Krzysztof Kotowicz PHP Developer http://web.eskot.pl OWASP Medycyna Praktyczna krzysztof@kotowicz.net 10.03.2010 Copyright © The OWASP Foundation Permission is granted to copy, distribute and/or modify this document under the terms of the OWASP License. The OWASP Foundation http://www.owasp.org
  • 2.
    Plan  What is SQL injection?  Why is it so dangerous (demo)?  How to defend? • Prepared statements • Escaping • Stored procedures • Additional methods  Summary OWASP 2
  • 3.
    Discussed databases (RDBMS)  MySQL  Oracle  MS SQL Server  To some extent: • PostgreSQL • SQLite OWASP 3
  • 4.
    Discussed PHP projects  PDO – PHP data objects • Common interface for various RDBMS  Doctrine 1.2 • ORM (Object Relational Mapper) used e.g. in Symfony framework  Propel 1.4 • ORM, like Doctrine • Used in Symfony  Zend Framework 1.10 • Popular framework MVC for PHP  MDB2 2.4.1 • Database abstraction layer (DBAL) • Distributed through PEAR OWASP 4
  • 5.
    What is SQLinjection? OWASP 5
  • 6.
    SQL injection –short definition It is a kind of web application attack, where user- supplied input coming from: URL: www.example.com?id=1 Forms: email=a@example.com Other elements: e.g. cookie, HTTP headers is manipulated so that vulnerable application executes SQL commands injected by attacker. OWASP 6
  • 7.
    Example – loginform SELECT * FROM users WHERE login = '{$login}' and password_hash = MD5('{$password}') $login = "' or 1=1 -- "; "anything"; $password = "dowolne"; // zamierzalismy osiagnacthis(kod dane) you wanted to achieve to (code data) SELECT * FROM users WHERE login = '' or 1=1 -- ' and password_hash = MD5('dowolne') MD5('anything') // but server interprets it as SELECT * FROM users WHERE login = '' or 1=1 -- ' and password_hash = MD5('anything') User logs in without knowing the login nor password OWASP 7
  • 8.
    Why is itso dangerous? DEMO OWASP 8
  • 9.
    What are thepossible threats?  Unauthorized access to application  Access to whole database / databases on the server  Denial of service  Database modification  Read / write files on server's filesystem  Code execution OWASP 9
  • 10.
    A few facts  Injection vulnerabilities are the 1st on OWASP Top 10 2010 RC  SQLi is responsible for 40–60% cases of data breach [1] [2]  Modern attack techniques are advanced and automated • Vulnerability is not only in WHERE part • Sometimes it is enough to break a query  Vulnerabilities are found on a daily basis, even in new applications OWASP 10
  • 11.
  • 12.
    How to defendagainst SQL injection?  Source of vulnerability is mixing code with data SELECT * FROM users WHERE login = 'login' Defense methods Separating code from data prepared statements stored procedures Escaping OWASP 12
  • 13.
    How to defend? Preparedstatements OWASP 13
  • 14.
    Prepared statements –how to use? 1. Preparing SQL command (string) Put placeholders where data should be WHERE a = ? ... WHERE a = :col 2. Send command to server PREPARE 3. Attach data to command 4. Execute command EXECUTE 5. Fetch results 3, 4, 5 could be repeated... 6. Clear the command OWASP 14
  • 15.
    Prepared statements -example  PDO // prepare command $stmt = $dbh->prepare("INSERT INTO SUMMARIES (name, sum) VALUES (:name, :sum)"); // attach data variables - WITH ITS TYPES! $stmt->bindParam(':name', $name, PDO::PARAM_STR); $stmt->bindParam(':sum', $sum, PDO::PARAM_INT); // bind values $name = 'something'; $value = 1234; // execute command $stmt->execute(); $stmt = null; //free memory OWASP 15
  • 16.
    Prepared statements -advantages  Commands are completely separated from data they operate on  Injection is not possible  Command is compiled only once - potential speedup $stmt->bindParam(':name', $name, PDO::PARAM_STR); $stmt->bindParam(':sum', $sum, PDO::PARAM_INT); // petla po danych... foreach ($do_bazy as $name => $value) { $stmt->execute(); } OWASP 16
  • 17.
    Prepared statements -caveats  Not all commands may be parametrised  You cannot put parameters everywhere -- error SELECT * FROM :table SELECT :function(:column) FROM :view -- not what you expect SELECT * FROM table WHERE :column = 1 SELECT * FROM table GROUP BY :column  Just using PS does not enforce using parameters in them  Sometimes they're emulated (it's a good thing!) OWASP 17
  • 18.
    Prepared statements inDoctrine  Uses PDO (emulated for Oracle) and prepared statements  Uses own DQL language instead of SQL $q = Doctrine_Query::create() ->select('u.id') ->from('User u') ->where('u.login = ?', ‘mylogin'); echo $q->getSqlQuery(); // SELECT u.id AS u__id FROM user u // WHERE (u.login = ?) $users = $q->execute(); OWASP 18
  • 19.
    Prepared statements inDoctrine cont.  It can still bite you $q = Doctrine_Query::create() ->update('Account') ->set('amount', 'amount + 200') ->where("id > {$_GET['id']}");  Correct this to: ->where("id > ?", (int) $_GET['id']);  NEVER put input data directly into SQL commands OWASP 19
  • 20.
    Prepared statements inPropel  Uses PDO, like Doctrine // through Criteria $c = new Criteria(); $c->add(AuthorPeer::FIRST_NAME, "Karl"); $authors = AuthorPeer::doSelect($c); // through custom SQL (sometimes it's more convenient) $pdo = Propel::getConnection(BookPeer::DATABASE_NAME); $sql = "SELECT * FROM complicated_sql JOIN some_big_join USING something WHERE column = :col)”; $stmt = $pdo->prepare($sql); $stmt->execute(array('col' => 'Bye bye SQLi!'); OWASP 20
  • 21.
    Prepared statements inZend Framework  PDO (+ mysqli + oci8 + sqlsrv) // prepare + execute $stmt = $db->prepare('INSERT INTO server (key, value) VALUES (:key,:value)'); $stmt->bindParam('key', $k); $stmt->bindParam('value', $v); foreach ($_SERVER as $k => $v) $stmt->execute(); // prepare + execute in one step $stmt = $db->query('SELECT * FROM bugs WHERE reported_by = ? AND bug_status = ?', array('goofy', 'FIXED')); while ($row = $stmt->fetch()) echo $row['bug_description']; OWASP 21
  • 22.
    Prepared statements inMDB2  Based on different database drivers (mysql, oci8, mssql, ...)  Emulates PS, if database doesn't support them $types = array('integer', 'text', 'text'); $stmt = $mdb2->prepare('INSERT INTO numbers VALUES (:id, :name, :lang)', $types); $data = array('id' => 1, 'name' => 'one', 'lang' => 'en'); $affectedRows = $stmt->execute($data); $stmt->free(); OWASP 22
  • 23.
    Prepared statements -summary  They offer very good protection (if used properly)  Easy to use, small changes in code  Good support in frameworks  They have their limits  Sometimes they have to be used with other defense methods OWASP 23
  • 24.
  • 25.
    Escaping – howdoes it work?  Data and commands are still kept in a single variable, but we try to separate them inline  Numbers • Cast to (int) / (float) – don't use is_numeric [1]!  Texts are surrounded with single quotes : ' .. WHERE col = 'TEXT DATA' AND ... • If quote is inside the text, you need a way to distinguish it from the ending quote • Prepend a special character e.g. "" to a quote • Escaping rules depend on context! OWASP 25
  • 26.
    Escaping – context addslashes() Returns a string with backslashes before characters that need to be quoted in database queries etc. These characters are single quote ('), double quote ("), backslash () and NUL (the NULL byte). / Source: php.net manual / $user = addslashes($_GET['u']); $pass = addslashes($_GET['p']); $sql = "SELECT * FROM users WHERE username = '{$user}' AND password = '{$pass}'"; $ret = exec_sql($sql);  Are you safe? OWASP 26
  • 27.
    NO OWASP
  • 28.
    Escaping – contextcont.  Different RDBMS have different ways of escaping data (it also depends on configuration)  addslashes() works just like MySQL only „by chance” RBDMS PHP function i've got quotes PDO $pdo->quote($val, $type) n/a (it depends) MySQL (mysql) mysql_real_escape_string i've got quotes MySQL (mysqli) mysqli_real_escape_string i've got quotes Oracle (oci8) n/d - str_replace() i''ve got quotes SQLite sqlite_escape_string i''ve got quotes MS SQL (mssql) n/d - str_replace() i''ve got quotes PostgreSQL pg_escape_string() i''ve got quotes OWASP 28
  • 29.
    Escaping – contextcont. // SELECT * FROM users WHERE username = // '{$user}' AND password = '{$pass}' $_GET['u'] = "anything'"; $_GET['p'] = " or 1=1 -- "; // MySQL sees it as : SELECT * FROM users WHERE username = 'anything'' AND password = ' or 1=1 -- ' // SQLite / MS SQL / Oracle / PostgreSQL: SELECT * FROM users WHERE username = 'anything'' AND password = ' or 1=1 -- '  Don't use addslashes(), use PHP functions for your RBDMS  Are you safe now? OWASP 29
  • 30.
    ALMOST OWASP
  • 31.
    Escaping gotchas –charsets  Errors discovered in 2006 in PostgreSQL and MySQL [1] [2]  In some multibyte charsets despite escaping you can cause SQL injection  is „swallowed” by multibyte character  Example: • BF 27 [ ¬ ' ]  BF 5C 27 [ ¬ ' ] • First 2 bytes are character ¿ in GBK charset • Server will see ¿' OWASP 31
  • 32.
    Escaping gotchas –charsets  Some Asian charsets are vulnerable  Luckily - not UTF-8!  In PostgreSQL '' escaping was used (instead of ')  In mysql_real_escape_string() escaping is done with respect to current connection charset • Doesn't always work! [1] [2]  Charset also defines context OWASP 32
  • 33.
    Escaping gotchas –object names  Colum, table, database etc. names • No common good rule to escape them • Different reserved words, different maximum name lengths etc. If you need to get those names from the user - use whitelisting (blacklisting if you really can't do otherwise) OWASP 33
  • 34.
    Escaping gotchas –object names cont.  Example - sorting by column  There's a vuln. in $order, but you can't escape there $cat_id = (int) $_GET['cid']; $order = $_GET['column']; $stmt = $pdo->prepare("SELECT * FROM products WHERE cid = :cid ORDER BY $order"); $stmt->bindParam(':cid', $cat_id, PDO::PARAM_INT); if ($stmt->execute()) { ... } OWASP 34
  • 35.
    Escaping gotchas –object names cont.  Whitelisting $columns = array( // list of allowed columns 'product_name','cid','price', ); if (!in_array($order, $columns, true)) $order = 'product_name'; // default column  Blacklisting // only a-z and _ $order = preg_replace('/[^a-z_]/', '', $order); // max 40 characters $order = substr($order, 0, 40); OWASP 35
  • 36.
    Escaping in PDO PDO::quote($value, $type, $len)  Length and type are sometimes ignored! • Cast numbers to (int), (float) • Texts – cut them manually $quoted = $pdo->quote($input, PDO::PARAM_STR, 40); OWASP 36
  • 37.
    Escaping in Doctrine Careful with Doctrine quote()! $q = Doctrine_Query::create(); // not like this!!! $quoted = $q->getConnection()->quote($input, 'text'); $q->update('User')->set('username', $quoted); // quote() only changes ' to '' - exploit (MySQL): $input = 'anything' where 1=1 -- '; // escape through PDO - getDbh(): $quoted = $q->getConnection() ->getDbh() ->quote($input, PDO::PARAM_STR); // 'anything ' where 1=1 -- ' OWASP 37
  • 38.
    Escaping in Propel Through PDO::quote() $pdo = Propel::getConnection(UserPeer::DATABASE_NAME); $c = new Criteria(); $c->add(UserPeer::PASSWORD, "MD5(".UserPeer::PASSWORD.") " ." = " . $pdo->quote($password), Criteria::CUSTOM); OWASP 38
  • 39.
    Escaping in ZendFramework  Functions quote(), quoteInto() $name = $db->quote("O'Reilly"); // 'O'Reilly' // simplified escaping for a single value $sql = $db->quoteInto("SELECT * FROM products WHERE product_name = ?", 'any string'); OWASP 39
  • 40.
    Escaping in MDB quote() // quote() function - give type $query = 'INSERT INTO table (id, itemname, saved_time) VALUES (' . $mdb2->quote($id, 'integer') .', ' . $mdb2->quote($name, 'text') .', ' . $mdb2->quote($time, 'timestamp') .')'; $res = $mdb2->exec($query); OWASP 40
  • 41.
    Escaping - summary  Looks easy - search and replace  Just looks • You need to know the context (database, charset) • There are invalid implementations  Encourages invalid practices • concatenating strings to form a SQL command • ignoring numeric parameters  Use only if • You program for a single RDBMS • There is no other way OWASP 41
  • 42.
    How to defend? Storedprocedures OWASP 42
  • 43.
    Stored procedures  SQL command(s) is moved to database server and stored there under a name  Client executes a procedure with input and output parameters  In output parameters client receives results  Data is formally separated from code  It's NOT enough OWASP 43
  • 44.
    Stored procedures cont. Example for MS SQL – a vulnerable procedure CREATE PROCEDURE SP_ProductSearch @prodname varchar(400) AS DECLARE @sql nvarchar(4000) SELECT @sql = 'SELECT ProductID, ProductName, Category, Price FROM Product Where ProductName LIKE ''' + @prodname + '''' EXEC (@sql) ...  It's just like eval()! OWASP 44
  • 45.
    Stored procedures cont. Same vulnerability in Oracle CREATE OR REPLACE PROCEDURE SP_ProductSearch(Prodname IN VARCHAR2) AS sqltext VARCHAR2(80); BEGIN sqltext := 'SELECT ProductID, ProductName, Category, Price FROM Product WHERE ProductName LIKE ''' || Prodname || ''''; EXECUTE IMMEDIATE sqltext; ... END; OWASP 45
  • 46.
    Stored procedures –Dynamic SQL  Vulnerability lies in Dynamic SQL • Data is again mixed with code in one variable  How to defend? • Separate the code from data • Escape OWASP 46
  • 47.
    Stored procedures inMS SQL  Separating code from data • use sp_executesql with parameter list CREATE PROCEDURE SP_ProductSearch @prodname varchar(400) = NULL AS DECLARE @sql nvarchar(4000) SELECT @sql = N'SELECT ProductID, ProductName, Category, Price FROM Product Where ProductName LIKE @p' EXEC sp_executesql @sql, N'@p varchar(400)', @prodname OWASP 47
  • 48.
    Stored procedures inMS SQL cont.  Escaping character data Object name QUOTENAME(@v) Text <= 128 chars QUOTENAME(@v,'''') Text > 128 chars REPLACE(@v,'''','''''')  Example: SET @cmd = N'select * from authors where lname=''' + REPLACE(@lname, '''', '''''') + N''''  Escape only when you must! (use sp_executesql with parameters) OWASP 48
  • 49.
    Stored procedures inOracle  Oracle - use EXECUTE IMMEDIATE .. USING CREATE OR REPLACE PROCEDURE SP_ProductSearch(Prodname IN VARCHAR2) AS sqltext VARCHAR2(80); BEGIN sqltext := 'SELECT ProductID, ProductName, Category, Price WHERE ProductName=:p'; EXECUTE IMMEDIATE sqltext USING Prodname; ... END;  Escaping - DBMS_ASSERT package OWASP 49
  • 50.
    Stored procedures inMySQL  Support for Dynamic SQL only through prepared statements  It's actually harder to make vulnerable procedure  Just use placeholders OWASP 50
  • 51.
    Stored procedures inMySQL cont.  PREPARE / EXECUTE USING / DEALLOCATE PREPARE DELIMITER $$ CREATE PROCEDURE get_users_like ( IN contains VARCHAR(40)) BEGIN SET @like = CONCAT("%", contains, "%"); SET @sql = "SELECT * FROM users WHERE uname LIKE ?"; PREPARE get_users_stmt from @sql; EXECUTE get_users_stmt USING @like; DEALLOCATE PREPARE get_users_stmt; END$$ DELIMITER ; OWASP 51
  • 52.
    Stored procedures inMySQL cont.  Or, even simpler DELIMITER $$ CREATE PROCEDURE get_users_like ( IN contains VARCHAR(40)) BEGIN SET @like = CONCAT("%", contains, "%"); SELECT * FROM users WHERE uname LIKE @like; END$$ DELIMITER ;  Escaping – QUOTE() function OWASP 52
  • 53.
    Stored procedures inPHP  Different support level, depending on RDBMS  Common API (e.g. PDO) only for simple calls • No return from procedure • Returns scalar value in OUT parameter  Different API (or none at all) for advanced calls • e.g. cursors, fetching records sets  Almost no support in frameworks  Still some errors... OWASP 53
  • 54.
    Stored procedures inPDO  Calling a procedure // MySQL $sql = "CALL get_users_like(:contains)"; // MS SQL – EXEC get_users_like :contains $stmt = $pdo->prepare($sql); $ret = $stmt->execute(array('contains' => $input)); foreach($stmt->fetchAll() as $users) { var_dump($users); } unset($s); OWASP 54
  • 55.
    Stored procedures in Doctrine/Propel/ZendFramework  Doctrine - no support (use PDO) $pdo = Doctrine_Manager::connection()->getDbh();  Propel – likewise $pdo = Propel::getConnection(UserPeer::DATABASE_NAME);  Zend Framework – likewise $pdo = $db::getConnection(); OWASP 55
  • 56.
    Stored procedures inMDB2  You need to manually escape all parameters! $mdb2->loadModule('Function'); $multi_query = $mdb2->setOption('multi_query', true); if (!PEAR::isError($multi_query)) { $result = $mdb2->executeStoredProc('get_users_like', array($mdb2->quote($contains, 'text'))); do { while ($row = $result->fetchRow()) { var_dump($row); } } while ($result->nextResult()); } OWASP 56
  • 57.
    Stored procedures -gotchas  Data length CREATE PROCEDURE change_password @loginname varchar(50), @old varchar(50), @new varchar(50) AS DECLARE @command varchar(120) SET @command= 'UPDATE users SET password=' + QUOTENAME(@new, '''') + ' WHERE loginname=' + QUOTENAME(@loginname, '''') + ' AND password=' + QUOTENAME(@old, '''') EXEC (@command) GO OWASP 57
  • 58.
    Stored procedures -summary  Moving SQL logic to server takes time  Code is not easily ported to other RDBMS  You need to use prepared statements or escaping to write safe stored procedures anyway  If done poorly, you're even more vulnerable • Both SP code and statement calling SP could be vulnerable • SP usually has greater permissions than code calling it  Bad support in PHP and frameworks OWASP 58
  • 59.
    Stored procedures -summary SPs have many advantages outside our scope  Could be used with different clients (Java/.NET + PHP)  Could have better berformance  and many more... Conclusion: You can write secure stored procedures, but they usually increase the application cost considerably It is vital to write stored procedures protected against SQL injection OWASP 59
  • 60.
    How to defend? Additionalmethods OWASP 60
  • 61.
    Validation and filtering  Validate all external data  Validate before processing  Filter INPUT - escape OUTPUT  Different validation rules for each parameter - check e.g. • Type • Scalar / array • Min / max values • Character data length! [1] OWASP 61
  • 62.
    Additional methods Complementary toall previously mentioned!  Principle of least privilege when connecting to DB  Removing unused functions, accounts, packages shipped with database  Routinely updating the system and database software  Correct PHP and database configuration • magic_quotes_* = false • display_errors = false  Good database design OWASP 62
  • 63.
    Summary  Pay attention to SQL injection - even a single mistake could cost you!  Prefer complete solutions - e.g. frameworks  Filter and validate all input data  Remeber about data types and lengths  Prefer whitelisting to blacklisting - the latter will fail one day!  Use prepared statements whenever you can  Try to avoid escaping  In stored procedures double check your Dynamic SQL OWASP 63
  • 64.
    Links  Discussed projects • sqlmap.sourceforge.net • php.net/manual/en/book.pdo.php • www.doctrine-project.org • propel.phpdb.org/trac • framework.zend.com • pear.php.net/package/MDB2  About SQL injection • www.owasp.org/index.php/SQL_Injection • unixwiz.net/techtips/sql-injection.html • delicious.com/koto/sql+injection  Hack me • threats.pl/bezpieczenstwo-aplikacji-internetowych • tinyurl.com/webgoat • mavensecurity.com/dojo.php  krzysztof@kotowicz.net http://blog.kotowicz.net OWASP 64