• Aucun résultat trouvé

The wildly popular and widely implemented legacy MySQL API, known as the mysql_query() series of functions, exists no more. It is depreciated as of PHP 5.5.

PDO, which stands for PHP Database Objects, is one of two database libraries that are to be used with MySQL going forward. Two reasons for moving on from the legacy MySQL API are an object-oriented interface to the database functions, and improved security. The first reason is great; the second is essential. Previously, it took a lot of manual effort to make mysql_query() secure. Even so, something could easily be missed, and often was. The libraries’ widespread use has enabled both great applications and widespread security holes. An automated way to assist with database security problems was needed. PDO is one of the answers (MySQLi being the other), and is the library chosen exclusively for this book.

The primary benefit of PDO for security programming is the prepared statement.

Prepared statements separate the construction of a SQL statement from the insertion of a query variable. Prepared statements prevent inserted user variables from altering the SQL statement, and they do this automatically, which is a great relief to defensive programming efforts, since the alteration of SQL statements is the hole that drives SQL injection.

The reason that the MySQL API is so susceptible to SQL injection is because the original mysql_query() function makes a single round trip to the MySQL server to obtain data. The call sends the query string with the embedded query variable to the server, which parses the SQL statement, compiles the new state-ment, and then executes it and returns the recordset. If the query variable is not properly escaped, the original SQL statement can be altered. This is the basis for SQL injection.

In PDO, true prepared statements are a two-step process and require two round trips to the MySQL server. First, the SQL statement is sent to the server, then parsed and compiled with placeholders for the variables. The statement is not executed. Then a second trip is made to the server with the user-supplied variables, which are then inserted into an already compiled query, executed, and a recordset is returned. This is a two-round-trip network call. In prepared statements, the SQL compiler never confuses user input as part of the compilation directions. Once prepared, the SQL statement cannot be altered.

Using prepared statements exclusively eliminates a great deal of manual escaping work and oversight negligence. Following the rules for calling prepared statements

has the additional benefit of causing the query to fail at run time, which is better than allowing a mistake to execute.

A few points to keep in mind when coding PDO are specifying the character set for the connection, which should be UTF-8 as per OWASP recommendations. It is important that the PDO client connection match the UTF-8 column type, and the data be stored as UTF-8.

Second, and this is not very clear from the documentation, is that PDO by default does not actually create true prepared statements. It emulates them by default. What does this mean? It means that instead of making two round trips, it makes only one. It does this by design for speed purposes. PDO properly escapes all variables before inserting into the SQL statement and then sends the escaped SQL statement to the server for compilation and execution. Data is returned in a single round trip. The power of this is that automatic escaping is achieved, as well as speed. Since many PHP/MySQL applications support high traffic loads, this is an important consideration.

Is that good enough? What about true prepared statements? First, it is good enough. Emulated statements can be trusted to get the job done correctly provided the character sets of the client connection and the data are matched.

There are no published security advisories about unsafe emulated statements, that is, if character encoding is set up correctly. If mysql_query() was automatically escaped, its security level would be very high. The problem is that this is a man-ual process which can be missed. Second, true prepared statements are an option.

Calling

pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

on a PDO connection turns true prepared statements on and causes two round trips to be made for every SQL query; one to prepare the statement and one to execute the statement with the parameters and return the result set.

The choice is left to the developer. As with anything, there is a compromise between speed and security. True prepared statements are a stronger measure and guarantee the separation of SQL statement compilation and execution. The speed cost of two round trips might not matter. On a high traffic site, with a highly frequent query, it might matter a great deal. Manual use of mysql_real_escape_string() is a successful escaping measure. The problem was never that the escaping function was unreliable or ineffective. It was highly effective. The problem was the huge amount of manual labor to implement it consistently across an application without the problems of oversight mistakes. PDO provides that measure of automation which is a real ben-efit, and the reason to move on with it into the future.

As a last thought on PDO prepared statements, remember, prepared statements automate the process of escaping data for database storage. The result is that SQL injection is prevented and data is preserved intact.

PDO UTF-8 Connection

Two basic preparations need to be made for secure database support. These are the creation of UTF-8 tables and a UTF-8 PDO connection. This is the correct way to open a PDO client connection with a UTF-8 character set.

$this->conn = new DO("mysql:host = {$host};dbname = {$db};charset = utf8",

$user, $pass);

It is subtle—and many examples miss including this option. In the first parameter, which is the DSN connection string, make sure to add 'charset = utf8'. This ensures that your emulated escaping mechanism is speaking UTF-8. As a defensive coder, the desired chain is UTF-8 data coming in, UTF-8 escaping at the client connection, and UTF-8 as the storage column so that characters are not altered when placed into the record.

The goal:

水能載舟,亦能覆舟 into DB 水能載舟,亦能覆舟 out of DB

Note: Chinese proverb—Not only can water float a boat, it can sink it also.

Here is the proper way to open a PDO connection. It needs to be surrounded by a try/catch exception handler because PDO always throws an exception on error, and it needs to be handled locally.

Try {

$this->conn = new PDO(

"mysql:host = {$host};dbname = {$db};charset = utf8",

$user, $pass);

$this->conn->setAttribute(

PDO::ATTR_ERRMODE,

PDO::ERRMODE_EXCEPTION);

$this->conn->setAttribute(

PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

}

catch(PDOException $e) {

$this->logErr($e->getMessage()); //log specific error to file header("Location: "serverDown.php"); // redirect user to generic

page }

This code opens a new PDO connection with UTF-8 character set, and sets the error mode going forward to throw exceptions, and to return records as an associative array.

By default, PDO returns records as both an associative array and an indexed array. By only using associative, some memory is saved. The exception handler first  logs a detailed error message to the log file, and then redirects users to a generic error page informing them that the service is temporarily down. Users need to be informed as to what is going on with the service, just not any details about it.

MySQL UTF-8 Database and Table Creation Support

Designing a database to hold UTF-8 characters is essential. The following examples show the MySQL syntax for UTF-8 database and table creation.

To create a UTF-8 database:

CREATE DATABASE users CHARACTER SET utf8 COLLATE utf8_general_ci To create a UTF-8 table:

CREATE TABLE 'members' (

'member_id' int(11) UNSIGNED NOT NULL auto_increment, 'name' varchar(255) CHARACTER SET utf8 NOT NULL default'', 'email' varchar(255) CHARACTER SET utf8 NOT NULL default'', 'activation_dt' TIMESTAMP NOT NULL default CURRENT_TIMESTAMP, PRIMARY KEY ('member_id'),

UNIQUE KEY 'email' ('email'))

ENGINE = INNODB DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci;

To alter an existing table for UTF-8:

ALTER TABLE members CONVERT TO CHARACTER SET utf8

Performance note: InnoDB keeps indexes in separate files. UTF-8 consumes more  space because it uses more bytes. This can affect both record size and index size. Smaller indexes and smaller records equal more data in memory and fewer disk seeks. If you know you have a high performance need and that the data (catalog data is an example) will always only ever have Latin characters, then set the column type to Latin1. It consumes less space and therefore consumes less memory.

Below is an example of dual character sets:

CREATE TABLE catalog (

desc VARCHAR(40) CHARACTER SET utf8,

title VARCHAR(20) CHARACTER SET latin1 COLLATE latin1_general_cs, PRIMARY KEY (title))

ENGINE = InnoDB;

PDO Prepared Statements

In addition to providing escaping protection, prepared statements can be executed several times with new variables without recompiling the SQL statement. There are two ways to use placeholders for prepared statements, named and unnamed. Named placeholders are specific and are easier to read and track. Unnamed placeholders can be a little harder to debug but allow more flexibility about which variable goes into the statement. Examples of both are listed here.

Named parameter placeholders:

$pdo->prepare("INSERT INTO members (name, email, id)

VALUES (:name, :email, :id)");

Unnamed parameter placeholders:

$pdo->prepare("INSERT INTO members (name, email, id) VALUES (?, ?, ?);

Prepared Statement Examples PDO Named Parameters Example //named parameter placeholders

$stmt = $pdo->prepare("INSERT INTO members (name, email, id) VALUES (:name, :email, :id)");

//bind the variables using the named placeholder syntax

$stmt->bindValue(':name', "MeloDee", PDO::PARAM_STR);

$stmt->bindValue(':email', baker@mobilesec.com, PDO::PARAM_STR);

$id = 2;

$stmt->bindValue(':id', $id, PDO::PARAM_INT);

$stmt->execute();

Three simple steps are performed. First, the SQL statement is prepared with named placeholders. Then values are bound to the named placeholders. The name of the first parameter in bindValue() is the exact name of the named placeholder. Since these are named, they do not need to be in order. The use of the preceding colon in the parameter name for bindValue is not necessary, but obviously the name needs to match. bindValue can bind to the value of a variable as well, as in the example above. The difference between bindValue() above, and bindParam(), in the example below is that bindValue() grabs the value at the time it was called. bind-Param() grabs whatever value is assigned to the variable when execute() is called.

bindParam() is essentially a reference to a variable, so the value of the variable can change, and bindParam() is updated accordingly. bindValue() also takes a third parameter that explicitly identifies the parameter type. By default, the parameter type is PDO::PARAM_STR. This is what enables the use of arrays with unspecified types in the next examples.

PDO Unnamed Parameters Example

$stmt = $pdo->prepare("INSERT INTO

members (name, email, id) VALUES (?, ?, ?)";

//bind variable to a parameter

//unnamed parameters are numbered by order //in this case 1, 2, 3

//by binding to variable,

//if the variable changes, the parameter changes

$stmt->bindParam(1, $name, PDO::PARAM_STR);

$stmt->bindParam(2, $email, PDO::PARAM_STR);

$stmt->bindParam(3, $id, PDO::PARAM_INT);

//insert first set of variables bound

$name = "Kam"

$email = "chef@mobilesec.com";

$id = "5";

$stmt->execute();

//change value of variables

//insert new set of values with same query

$name = "Wendy"

$email = "beautifulness@mobilesec.com";

$id = "1";

$stmt->execute();

Again, three steps are performed. First, the SQL statement is with three placeholders.

Second, variables are bound to the placeholders via bindParam();. It is important to note that numerical order is required here as indicated by the first parameter to bindParam();. Since the placeholders are not named, order is important. If the variable order does not match the column order in the statement, the query will fail.

Third, once the values are bound to the query, it is executed. Steps two and three are then repeated ad infinitum with the same prepared statement which only needed to be compiled once. If repetitive calls are made via this method, this does result in faster performance over a non-prepared statement, which would have to be compiled each time the parameter values changed.

In the example above, three round trips would be made—one to compile the state-ment and two more to execute and return two different result sets. A non-prepared statement would have resulted in two round trips, and two query compilations. If three queries or more queries had been run, the difference gap starts to widen.

The usefulness of unnamed placeholders is in being able to use an array of values as in,

$user = array('Robert', 'photog@mobilesec.com', '8');

$stmt = $pdo->prepare("INSERT INTO members (name, email, id) VALUES (?, ?, ?)";

$stmt->execute($user);

Note: The order of the array must also match the order of the table columns or an error will result. This is not the case with named parameters.

PDO Class Objects Example class member {

public $name;

public $email;

public $id;

function _construct($name, $email, $id) {

$this->name = $name;

$this->email = $email;

$this->id = $id;

}

function getAccountInfo(){

//retrieve private data }

}

$regUser = new member('Mark', 'engineer@mobilesec.com', '35');

$stmt = $pdo->prepare("INSERT INTO members (name, email, id) VALUES (:name, :email, :id)");

$stmt->execute((array)$regUser); //NOTICE the array cast

This example shows how PDO accommodates objected-oriented programming by allowing a living object to be placed into the query and executed. Casting the object to an array is the mechanism that lines up the named placed holders with the private member variables of the live object $regUser of the class “member.” To make use of this capability the names and order of the object must match the placeholder names and order of the SQL statement.

Selecting Data and Placing into HTML and URL Context

Below is an example of selecting data with a PDO SELECT prepared statement and placing the results into HTML.

<?php

$s tmt = $pdo->prepare('SELECT name, email, id FROM members WHERE id = :id');

$stmt->execute(array('id' = > $id));

function _H($html) {echo htmlspecialchars($ html, ENT_QUOTES,

"UTF-8");}

function _UH($url) {echo htmlspecialchars(u rlencode($url),

ENT_QUOTES, "UTF-8";}

//end PHP, Begin straight HTML

<?php while($row = $stmt->fetch()) {?>

<tr >

< a href = "mobilesec/profile.php?id = <?php _UH($row['id']);

?>">

There are several important parts of this example. First, the SELECT statement is prepared and called with an associative array matching the named placeholder in the pre-pared statement. Second, output escaping functions are set up with shorthand wrappers to ease the placement of in location output escaping in the HTML. Third, the PHP pro-cessing is separated from the HTML. When the PHP propro-cessing ends, straight HTML is output, making it cleaner to format, and cleaner to examine and inspect. Fourth, the PDO statement is looped through to grab the results and place them into the HTML using inline PHP tags and being escaped in-location via the calls to the escaping wrap-per functions _H(), and _UH(). Notice that $row['name' and $row['email'] are being escaped for HTML context, and that the second reference to $row['name'] is being escaped for two contexts. It is escaped first as a URL parameter context as part of the anchor tags HREF attribute, and displayed in an HTML context. Two escaping processes need to occur here. URLs need a different escaping process than HTML does, therefore the _UH wrapper first calls urlencode() to prepare the variable for the con-text of a URL parameter, then htmlentities() is called to prepared it for display in the HTML context. Fifth, notice that the HREF attribute is enclosed by double quotes.

A rundown of the security process occurring here is as follows. urlencode() prevents issues like spaces in variables from becoming a parsing problem. If for example, the $id parameter contained the value “John Doe”, urlencode() would turn it into “John+Doe”. After this occurs, htmlentities() will escape any HTML entities that exist within the variable. What is wanted in this case though is to prevent

the user variable from breaking out of the quoted HREF attribute. It could do this if it contained a double. If left unescaped, the variables quote could prematurely end the HREF attribute, and begin a new attribute of its own. By using ENT_QUOTES with htmlentities() any quotes are escaped, therefore any user input is imprisoned within the double quoted attribute.

Note: It is ironic that the manual labor involved in explicitly escaping the variable for display context is the exact problem that prepared statements solved for SQL queries.

PDO SELECT Queries and Class Objects

PDO also lets you return records as full-fledged objects as this example shows.

class member { public $name;

public $email;

public $id;

function _construct($name, $email, $id) {

$this->name = $name;

$this->email = $email;

$this->id = $id;

}

function printName(){

return $this->name;

} }

$stmt = $pdo->prepare('SELECT name, email, id FROM members

WHERE id = :id');

$stmt->setFetchMode(PDO::FETCH_CLASS, "member");

$stmt->execute(array('id' = > $id));

while($obj = $stmt->fetch()) {

echo $obj->printName(); //command line output context }

Quoting Values and Database Type Conversion

With PDO prepared statements, there are three ways to bind parameters to the MySQL query statement.

$stmt->bindValue(:id, $id, PDO::PARAM_INT);

$stmt->execute();

Or

$stmt->bindValue(:id, $id);

$stmt->execute();

Or

$stmt->execute(array('id' = > $id));

What is the difference? Why does this work without security problems? The reason is that internally, all parameters are strings to MySQL. MySQL converts parameters to the appropriate type when needed by the type specification of the table column.

This is the type declared in the CREATE TABLE syntax. If the table column type is CHAR, the string is inserted. If the table type is INT, the string is converted to an integer type and then inserted into the column.

An example of this auto-type conversion by MySQL is coded below. Assuming that the ‘name’ column type is CHAR, and that the ‘id’ column type is INT

SELECT name, email, id FROM members WHERE id = 5 //fine SELECT name, email, id FROM members WHERE id = "5" //fine SELECT name, email, id FROM members WHERE name = "5"//fine SELECT name, email, id FROM members WHERE name = 5 //error

With PDO, the first example is very specific about type. The fact is that this does not, outside of being pedantic (which is always good) matter so much for MySQL.

However, since PDO is a wrapper for other database engines, it might matter a great deal. PDO, internally, will know about the binding requirements of the database engine it is working with. Specificity helps here. If your application will only ever be MySQL, then the second two options are just as good.

The second example, since it is not specified, treats the :id parameter as a string type which will automatically be converted by MySQL upon processing once MySQL knows the actual column type.

The third example, unlike the previous two, does not use bindValue() at all.

Instead it takes a dynamically created array as a parameter. This array must be

Instead it takes a dynamically created array as a parameter. This array must be