Skip to content

parseDsn() does not URL-decode username/password, breaking encoded DSN credentials #19557

Description

@dereuromark

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions