Description
StaticConfigTrait::parseDsn() never URL-decodes the userinfo (username / password) or path parts of a DSN, so a correctly percent-encoded password is passed through verbatim and the connection fails.
This is the same class of problem as cakephp/phinx#2421. Since cakephp/migrations 5.x dropped the Phinx dependency and now routes DSN strings through ConnectionManager::parseDsn() (see ManagerFactory::getManager()), the behavior is inherited from core rather than fixed by the Phinx removal.
Reproduce (CakePHP 5.3.x):
use Cake\Datasource\ConnectionManager;
$config = ConnectionManager::parseDsn('mysql://root:p%40ssword@localhost:3306/mydb?charset=utf8');
// $config['password'] === 'p%40ssword'
// expected: 'p@ssword'
$config = ConnectionManager::parseDsn('mysql://us%3Aer:pa%2Fss@localhost/mydb');
// $config['username'] === 'us%3Aer', $config['password'] === 'pa%2Fss'
Two things that make this worse than it looks
1. Query arguments are decoded, userinfo is not.
parseDsn() runs the query string through parse_str(), which decodes. So within one DSN, half the values are decoded and half are not:
$config = ConnectionManager::parseDsn('mysql://root:pw@localhost/mydb?ssl_key=%2Fpath%2Fto%2Fkey');
// $config['ssl_key'] === '/path/to/key' (decoded)
// $config['password'] === 'pw' (would stay encoded if it contained %XX)
parse_str() also turns + into a space, which the userinfo part does not do.
2. Raw special characters partially work today, which hides the problem.
The hand-rolled regex in parseDsn() has a host group of [^?#/:@]+, so it backtracks and a raw at-sign in the password happens to parse correctly:
$config = ConnectionManager::parseDsn('mysql://root:p@ssword@localhost:3306/mydb');
// $config['password'] === 'p@ssword' (works)
Raw /, ? and # in a password still break, since they are consumed by the path / query / fragment groups. So users get pushed toward percent-encoding, which is then not decoded. There is no spelling of such a password that works.
Scope
StaticConfigTrait is shared, so this is not migrations-specific. It affects every DSN consumer:
Cake\Datasource\ConnectionManager
Cake\Mailer\TransportFactory (SMTP passwords)
Cake\Cache\Cache
Cake\Log\Log
Suggested direction (6.x)
Decode the userinfo and path components, as Doctrine DBAL's DsnParser does: https://github.com/doctrine/dbal/blob/ff5c1863b4dc16ab900d6552c8401dcf1ae3975f/src/Tools/DsnParser.php#L54
Filing this against 6.0 rather than 5.x because it is a behavior change: a password containing a literal % is currently passed through unchanged and works, and would start being interpreted as an escape sequence. That needs a release note, and possibly a migration guide entry, rather than a patch release.
Happy to send a PR if the direction is agreed.
CakePHP Version
5.3.x (current 6.x parseDsn() is unchanged)
PHP Version
8.5
Description
StaticConfigTrait::parseDsn()never URL-decodes the userinfo (username/password) or path parts of a DSN, so a correctly percent-encoded password is passed through verbatim and the connection fails.This is the same class of problem as cakephp/phinx#2421. Since
cakephp/migrations5.x dropped the Phinx dependency and now routes DSN strings throughConnectionManager::parseDsn()(seeManagerFactory::getManager()), the behavior is inherited from core rather than fixed by the Phinx removal.Reproduce (CakePHP 5.3.x):
Two things that make this worse than it looks
1. Query arguments are decoded, userinfo is not.
parseDsn()runs the query string throughparse_str(), which decodes. So within one DSN, half the values are decoded and half are not:parse_str()also turns+into a space, which the userinfo part does not do.2. Raw special characters partially work today, which hides the problem.
The hand-rolled regex in
parseDsn()has a host group of[^?#/:@]+, so it backtracks and a raw at-sign in the password happens to parse correctly:Raw
/,?and#in a password still break, since they are consumed by the path / query / fragment groups. So users get pushed toward percent-encoding, which is then not decoded. There is no spelling of such a password that works.Scope
StaticConfigTraitis shared, so this is not migrations-specific. It affects every DSN consumer:Cake\Datasource\ConnectionManagerCake\Mailer\TransportFactory(SMTP passwords)Cake\Cache\CacheCake\Log\LogSuggested direction (6.x)
Decode the userinfo and path components, as Doctrine DBAL's
DsnParserdoes: https://github.com/doctrine/dbal/blob/ff5c1863b4dc16ab900d6552c8401dcf1ae3975f/src/Tools/DsnParser.php#L54Filing this against 6.0 rather than 5.x because it is a behavior change: a password containing a literal
%is currently passed through unchanged and works, and would start being interpreted as an escape sequence. That needs a release note, and possibly a migration guide entry, rather than a patch release.Happy to send a PR if the direction is agreed.
CakePHP Version
5.3.x (current 6.x
parseDsn()is unchanged)PHP Version
8.5