Why does PDO print my password when the connection fails?
I have a simple website where I establish a connection to a MySQL server using PDO.
$dbh = new PDO('mysql:host=localhost;dbname=DB;port=3306',
'USER',
'SECRET',
array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
I had some traffic on my site and the server's connection limit was reached, and the website throws this error, with my plain password in it!
Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[08004] [1040] Too many connections' in /home/domain/html/index.php:xxx Stack trace: #0 /home/domain/html/index.php(64): PDO->__construct('mysql:host=loca...', 'USER', 'SECRET', Array) #1 {main} thrown in /home/domain/html/index.php on line 64
Ironically I switched to PDO for security reasons, so this really shocked me, because this exact error is something you can provoke very easily on most sites using simple HTTP flooding.
I have now wrapped my connection in a try/catch block, but still I think this is catastrophic!
I am new to PDO and so my question is: what do I have to do to consider to be safe? How do I establish a connection in a secure way? Are there other known security holes like this one that I have to be aware of?
Solution 1:
You should have display_errors = off
in your PHP.ini anyway to avoid this problem. Errors that reveal details like these come from many places, in addition to PDO.
Yes, you should also have it in a try/catch block.
You can also $pdo->setAttribute(PDO::ERRMODE_SILENT)
, but then you need to be checking the error codes manually rather than using a try/catch block. See http://php.net/manual/en/pdo.setattribute.php for more error constants.
Solution 2:
A simple workaround is to catch the PDOException thrown by the PDO constructor:
try {
$dbh = new PDO('mysql:host=localhost;dbname=DB;port=3306', 'USER',
'SECRET', array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
} catch (PDOException $e) {
throw new Exception($e->getMessage());
}
Solution 3:
Ok, this made me giggle a little. The usage of error reporting is for debugging purposes, and it allows you to quickly find and fix issues.
When you're within a live environment your server should be configured for internal logging only, and not direct output, so basically you will need to turn off the output of errors within your php.ini
.
display_errors = Off
But while you're within your test environment, this stack is merely a tool to help you and is configurable.
When errors occur within a live environment they would be logged, so you should always be checking your log files and then fix accordingly.
People may specify that you can manage errors within your PHP application, but by personal preference I think this is the wrong way to go about it. Configuring the INI and configuration files for your web-server and MySQL / SQL Server will result in more acute management.
If your application is a public application then it would also be a good idea to handle errors within the application as a large percentage of clients may be on shared hosting and not have full access to server configurations.
Solution 4:
We use encoded username and passwords, and decode those in the PDO constructor. Then we catch the PDOException and throw a new PDOException with the old exception its message, so that the trace will show only the encoded username and password.
A good encryption library for PHP is defuse/php-encryption.
Example code:
<?php
class myPDOWrapper extends PDO
{
public function __construct(string $dns, string $encodedUser, string $encodedPassword)
{
try {
parent::__construct($dns, $this->decodeFunction($encodedUser), $this->decodeFunction($encodedPassword),
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]
);
}
catch (PDOException $exception) {
throw new PDOException($exception->getMessage());
}
}
private function decodeFunction(string $encoded): string
{
return \Defuse\Crypto\Crypto::decrypt($encoded, $this->decodeKey());
}
private function decodeKey(): \Defuse\Crypto\Key
{
static $key = null;
if(null === $key) {
$key = \Defuse\Crypto\Key::loadFromAsciiSafeString(getenv('MY_PDO_DECODE_KEY'));
}
return $key;
}
}