Description
Obviously it's possible to use a format like example.net:3306
as host argument for mysqli_real_connect()
. This works, but it has some serious issues with SSL certificate verification.
Maybe this bug is not limited to MySQLi.
Prerequisites
- MySQL or MariaDB server with SSL enabled
- Your own CA and certs/keys for server and client
- Working MySQL account with
REQUIRE SSL
Example
This short example demonstrates the bug:
<?php
$mysql = mysqli_init();
$host = 'example.net:3306';
$flags = MYSQLI_CLIENT_SSL;
mysqli_ssl_set($mysql, 'x509.key', 'x509.pem', 'x509.ca', null, null);
mysqli_real_connect($mysql, $host, 'username', 'password', null, null, null, $flags);
PHP 8.1 output
PHP Warning: mysqli_real_connect(): Cannot connect to MySQL by using SSL in …
PHP Fatal error: Uncaught mysqli_sql_exception: (trying to connect via (null)) in …
Null? Ok, let's retry with older PHP releases…
PHP 7.0 output
PHP Warning: mysqli_real_connect(): Cannot connect to MySQL by using SSL in …
PHP Warning: mysqli_real_connect(): [2002] (trying to connect via tcp://example.net:3306:3306) in …
PHP Warning: mysqli_real_connect(): (HY000/2002): in …
Note the doubled port! This is not a C&P error.
Workarounds
Removing the port from the host argument is a possible workaround.
<?php
$mysql = mysqli_init();
$host = 'example.net';
$flags = MYSQLI_CLIENT_SSL;
mysqli_ssl_set($mysql, 'x509.key', 'x509.pem', 'x509.ca', null, null);
mysqli_real_connect($mysql, $host, 'username', 'password', null, 3306, null, $flags);
Using MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT
is another workaround, but that's not a good solution.
<?php
$mysql = mysqli_init();
$host = 'example.net:3306';
$flags = MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
mysqli_ssl_set($mysql, 'x509.key', 'x509.pem', 'x509.ca', null, null);
mysqli_real_connect($mysql, $host, 'username', 'password', null, null, null, $flags);
Not using mysqli_ssl_set()
is one more workaround, but now we don't have client certificates anymore.
<?php
$mysql = mysqli_init();
$host = 'example.net:3306';
$flags = MYSQLI_CLIENT_SSL;
mysqli_real_connect($mysql, $host, 'username', 'password', null, null, null, $flags);
Conclusion
I think only the first port occurrence in the connection string will be used:
mysqli_real_connect($mysql, 'example.net:3301', 'username', 'password', null, 3302, null, MYSQLI_CLIENT_SSL);
Results internally in tcp://example.net:3301:3302
and opens a TCP connection to example.net:3301
. Now the CommonName (or SAN) of the certificate will be checked against example.net:3301
.
Suggestions
Some personal ideas…
- Don't internally add a port to the connection string, if it already contains a port. – or –
- Remove port suffix from host if dedicated port argument isn't null. – or –
- Extract the port from host argument and remove it. – or –
- Ignore port suffix at certificate verification.
PHP Version
PHP 8.1.7 with mysqlnd (deb.sury.org)
Operating System
Debian 11 (Bullseye)