It was the year 2016. A business owner just had a great idea to expand their reach in the marketplace, and they needed to build a web application to pull it off. After a bit of market research, they decide the best course of action is to hire PHP programmers to build their vision. Let's say you're a PHP programmer. A few days later, you find yourself employed at the company to make this vision happen. As you're sitting at the desk reviewing the wireframes that were agreed upon, the news on TV says something that catches your ear. You turn the volume up and learn that another company got hacked, all their customers' data was compromised, and it's just a mess all around. You look back at your wireframes and wonder, "If it were to happen to this project, how would it fare?" How would you even begin to answer this question? ## Understanding the Essence of Security Please set aside most of what you've heard over the years; chances are, most of it just muddies the water. [Security is not a product](https://googleprojectzero.blogspot.com/2016/06/how-to-compromise-enterprise-endpoint.html). Security is not a checklist. Security is not an absolute. **Security is a process.** Security is an emergent property of a mature mindset in the face of risk. Perfect security is not possible, but attackers do have budgets. If you raise the cost of attacking a system (your application or the networking infrastructure it depends on) so high that the entities that would be interested in defeating your security are incredibly unlikely to succeed, you'll be incredibly unlikely to be compromised. Even better, the most effective ways to raise the cost of attack against your system don't significantly increase the cost of using the system legitimately. This is no accident; as Avi Douglen says, "Security at the expense of usability, comes at the expense of security." With that in mind, let's look at some easy security wins you can implement in your PHP applications that significantly raise the cost of attack without making your software unusable. ## Improve PHP Security in 4 Easy Steps
<?php
/** PHP 5 snippet */
class Foo {
public function get_products($name, $start = 0, $end = 30)
{
$name = mysql_real_escape_string($name);
$query = mysql_query("SELECT * FROM products WHERE name LIKE '%{$name}%' LIMIT {$start}, {$end}");
$products = [];
while ($row = mysql_fetch_assoc($query)) {
$products []= $row;
}
return $products;
}
}
$foo = new Foo;
$products = $foo->get_products($_GET['search']);
This appears secure (we're escaping strings), but what if you accidentally passed a GET parameter as the second and third arguments to `$foo->get_products()`? Chances are, [SQL injection](https://paragonie.com/blog/2015/05/preventing-sql-injection-in-php-applications-easy-and-definitive-guide) would be the result.
Now let's look at the PHP 7 approach:
<?php
declare(strict_types=1);
/** PHP 7 snippet */
class Foo
{
protected $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
$this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function get_products(string $name, int $start = 0, int $end = 30): array
{
$stmt = $this->pdo->prepare(
"SELECT * FROM products WHERE name LIKE :name LIMIT {$start}, {$end}"
);
$stmt->execute([
'name' =>
'%' . $name . '%'
]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
$foo = new Foo(new PDO(/* ... */));
$products = $foo->get_products($_GET['search'] ?? '');
Is this more secure than the PHP 5 snippet? Certainly. If a developer accidentally passes a GET parameter to the second or third arguments, this will throw a `TypeError` unless they cast the user's input to an integer first. If the script doesn't catch the `TypeError`, then the script aborts execution. Type safety is useful for stopping unintended consequences.
There are no known integer inputs that can result in SQL injection, given the code above. However, it's still highly recommended to **avoid string concatenation in SQL queries at all**.
<?php
declare(strict_types=1);
/**
* Better PHP 7 snippet. If you're going to reference anything for
* your own development, this is the snippet you want.
*/
class Foo
{
/**
* @var \PDO $pdo
*/
protected $pdo;
/**
* @param PDO $pdo
*/
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
$this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
/**
* Get a list of products, with pagination support.
*
* Note that this LIMIT syntax is almost exclusive to MySQL. For PostgreSQL,
* you'll want to use "OFFSET :start LIMIT :end"
*
* @param string $name
* @param int $start
* @param int $end
* @return array
*/
public function get_products(string $name, int $start = 0, int $end = 30): array
{
$stmt = $this->pdo->prepare(
"SELECT * FROM products WHERE name LIKE :name LIMIT :start, :end"
);
$stmt->execute([
'name' =>
'%' . $name . '%',
'start' =>
$start,
'end' =>
$end
]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
$foo = new Foo(new PDO(/* ... */));
$products = $foo->get_products($_GET['search'] ?? '');
Furthermore, both the MySQLi and PDO extensions support a feature called [prepared statements](https://paragonie.com/blog/2015/05/preventing-sql-injection-in-php-applications-easy-and-definitive-guide). The old MySQL extension did not.
PHP 7 marked an inflection point in the PHP language (and the ecosystem surrounding it) to make it easier to do the secure thing than the insecure thing.
<?php
use ParagonIE\CSPBuilder\CSPBuilder;
(new CSPBuilder())
->addSource('image', 'https://ytimg.com')
->addSource('frame', 'https://youtube.com')
->addSource('script', 'https://www.google.com')
->addDirective('upgrade-insecure-requests', true)
->sendCSPHeader();
At bare minimum, you should send these headers:
* [`Strict-Transport-Security`](https://scotthelme.co.uk/hsts-the-missing-link-in-tls/)
* [`X-Frame-Options`](https://scotthelme.co.uk/hardening-your-http-response-headers/#x-frame-options)
* [`X-XSS-Protection`](https://scotthelme.co.uk/hardening-your-http-response-headers/#x-xss-protection)