Learn what is SQL injection, how to use prepared statements, how to escape and write secure stored procedures. Many PHP projects are covered - PDO, Propel, Doctrine, Zend Framework and MDB2. Multiple gotchas included.
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
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
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 – 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
9. 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
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
12. 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
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
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 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
19. 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
20. 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
21. 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
22. 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
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
25. 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
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
28. 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
29. 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
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 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
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
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 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
48. 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
49. 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
50. 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
51. 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
52. 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
53. 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
54. 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
56. 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
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
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 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
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