PHP MySQLi Prevent SQL Injection

PHP MySQLI Prevent SQL Injection

Any query can be injected whether it's read or write, persistent or transient. Injections can be performed by ending one query and running a separate one (possible with mysqli), which renders the intended query irrelevant.

Any input to a query from an external source whether it is from users or even internal should be considered an argument to the query, and a parameter in the context of the query. Any parameter in a query needs to be parameterized. This leads to a properly parameterized query that you can create a prepared statement from and execute with arguments. For example:

SELECT col1 FROM t1 WHERE col2 = ?

? is a placeholder for a parameter. Using mysqli, you can create a prepared statement using prepare, bind a variable (argument) to a parameter using bind_param, and run the query with execute. You don't have to sanitize the argument at all (in fact it's detrimental to do so). mysqli does that for you. The full process would be:

$stmt = $mysqli->prepare("SELECT col1 FROM t1 WHERE col2 = ?");
$stmt->bind_param("s", $col2_arg);
$stmt->execute();

There is also an important distinction between parameterized query and prepared statement. This statement, while prepared, is not parameterized and is thus vulnerable to injection:

$stmt = $mysqli->prepare("INSERT INTO t1 VALUES ($_POST[user_input])");

To summarize:

  • All Queries should be properly parameterized (unless they have no parameters)
  • All arguments to a query should be treated as hostile as possible no matter their source

prevent sql injection in mysqli

As already stated in comments, you need to be consistent with your API choice. You can't mix APIs in PHP.

You started out with mysqli_*, so I'll continue with that. You had some mysql_* and PDO in there, and it might not be a bad idea to use PDO over mysqli_* - but if your server supports mysqli_*, there is nothing wrong with using that. See Choosing an API and decide for yourself (just stay away from mysql_*, it's outdated).

Using mysqli_*, you connect to the database like this (you didn't show your connection).

$mysqli = new mysqli("host", "username", "password", "database");
if ($mysqli->connect_errno) {
echo "Failed to connect to MySQL: (".$mysqli->connect_errno.") ".$mysqli->connect_error;
}
$mysqli->set_charset("utf8");

As for preventing SQL injection in it self, all you need is to use prepared statements. You can still clean or sanitize your data if there are some kind of values you don't want sitting in your tables - but that's kind of another discussion.

You also need to know if your passwords are hashed in the database. They really should be, and you should be using password_hash($password, $algorithm) and password_verify($password, $hash) if you're on PHP5.5 and above (if not, look into something like password_compat).

You need to be consistent with your hashes too, you can't insert it with md5 and selecting it with no hash. It all needs to be the same. Because if you are selecting an md5 hash, and comparing it to an unhashed string, they will be different, and the query fails.

I'm showing you an example of using password_verify(), so that means that the password stored in the database will also need to be stored with password_hash() (or your query fails).

if ($stmt = $mysqli->prepare("SELECT uid, password FROM users where email=?")) {
$stmt->bind_param("s", $_POST['email']); // Bind variable to the placeholder
$stmt->execute(); // Execute query
$stmt->bind_result($userID, $password); // Set the selected columns into the variables
$stmt->fetch(); // ...and fetch it
if ($stmt->num_rows) {
if (password_verify($_POST['password'], $password)) {
// Password was correct and matched the email!
} else {
// Password was incorrect...
}
} else {
// Accountname not found
}
}

This is just a basic example, but it will get you started. Never trust user input, use prepared statements.

PHP / MySQLi: How to prevent SQL injection on INSERT (code partially working)

In the New PHP code snippet, you are still vulnerable to injections.

You are using a prepared statement in the insert part, but you are not actually using the preparations strengths correctly.

When creating a prepared statement, you create a query in which you add placeholders instead of the raw values:

$stmt = $conn->prepare("INSERT INTO Users (email, pw) VALUES (?, ?)");

The question marks are the placeholders and are later replaced by using the bind_param method:

$stmt->bind_param('ss', $email, $pw);

The ss part of the bind call tells the mysql db that its two strings that are passed to the database (s for string, i for int etc).

You are binding a param ($name) but it has no placeholder nor any type of reference in the query..?

Your select statement on the other hand is still unsafe and open to vulnerabilities.

I would probably use a prepared statement there to, just as with the insert part.

You always want to make sure that input from the user is "safe" for the database, if you concat a query string and add user input into it, the database will not escape the strings, it will just run it.

Only use standard query method calls when you write the full query yourself, without any input params, and especially no input params that the user passed!

How can I prevent SQL injection in PHP?

The correct way to avoid SQL injection attacks, no matter which database you use, is to separate the data from SQL, so that data stays data and will never be interpreted as commands by the SQL parser. It is possible to create an SQL statement with correctly formatted data parts, but if you don't fully understand the details, you should always use prepared statements and parameterized queries. These are SQL statements that are sent to and parsed by the database server separately from any parameters. This way it is impossible for an attacker to inject malicious SQL.

You basically have two options to achieve this:

  1. Using PDO (for any supported database driver):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    $stmt->execute([ 'name' => $name ]);

    foreach ($stmt as $row) {
    // Do something with $row
    }
  2. Using MySQLi (for MySQL):

    $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
    $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    $stmt->execute();

    $result = $stmt->get_result();
    while ($row = $result->fetch_assoc()) {
    // Do something with $row
    }

If you're connecting to a database other than MySQL, there is a driver-specific second option that you can refer to (for example, pg_prepare() and pg_execute() for PostgreSQL). PDO is the universal option.



Correctly setting up the connection

PDO

Note that when using PDO to access a MySQL database real prepared statements are not used by default. To fix this you have to disable the emulation of prepared statements. An example of creating a connection using PDO is:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8mb4', 'user', 'password');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

In the above example, the error mode isn't strictly necessary, but it is advised to add it. This way PDO will inform you of all MySQL errors by means of throwing the PDOException.

What is mandatory, however, is the first setAttribute() line, which tells PDO to disable emulated prepared statements and use real prepared statements. This makes sure the statement and the values aren't parsed by PHP before sending it to the MySQL server (giving a possible attacker no chance to inject malicious SQL).

Although you can set the charset in the options of the constructor, it's important to note that 'older' versions of PHP (before 5.3.6) silently ignored the charset parameter in the DSN.

Mysqli

For mysqli we have to follow the same routine:

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // error reporting
$dbConnection = new mysqli('127.0.0.1', 'username', 'password', 'test');
$dbConnection->set_charset('utf8mb4'); // charset


Explanation

The SQL statement you pass to prepare is parsed and compiled by the database server. By specifying parameters (either a ? or a named parameter like :name in the example above) you tell the database engine where you want to filter on. Then when you call execute, the prepared statement is combined with the parameter values you specify.

The important thing here is that the parameter values are combined with the compiled statement, not an SQL string. SQL injection works by tricking the script into including malicious strings when it creates SQL to send to the database. So by sending the actual SQL separately from the parameters, you limit the risk of ending up with something you didn't intend.

Any parameters you send when using a prepared statement will just be treated as strings (although the database engine may do some optimization so parameters may end up as numbers too, of course). In the example above, if the $name variable contains 'Sarah'; DELETE FROM employees the result would simply be a search for the string "'Sarah'; DELETE FROM employees", and you will not end up with an empty table.

Another benefit of using prepared statements is that if you execute the same statement many times in the same session it will only be parsed and compiled once, giving you some speed gains.

Oh, and since you asked about how to do it for an insert, here's an example (using PDO):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute([ 'column' => $unsafeValue ]);


Can prepared statements be used for dynamic queries?

While you can still use prepared statements for the query parameters, the structure of the dynamic query itself cannot be parametrized and certain query features cannot be parametrized.

For these specific scenarios, the best thing to do is use a whitelist filter that restricts the possible values.

// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}


Related Topics



Leave a reply



Submit