Skip to content

Commit 731e2d0

Browse files
committed
Fail authentication when server requests unknown authentication plugin
1 parent d68b506 commit 731e2d0

File tree

4 files changed

+102
-11
lines changed

4 files changed

+102
-11
lines changed

src/Commands/AuthenticateCommand.php

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
/**
99
* @internal
10-
* @link https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse
10+
* @link https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_response.html#sect_protocol_connection_phase_packets_protocol_handshake_response41
1111
*/
1212
class AuthenticateCommand extends AbstractCommand
1313
{
@@ -73,8 +73,19 @@ public function getId()
7373
return 0;
7474
}
7575

76-
public function authenticatePacket($scramble, Buffer $buffer)
76+
/**
77+
* @param string $scramble
78+
* @param ?string $authPlugin
79+
* @param Buffer $buffer
80+
* @return string
81+
* @throws \UnexpectedValueException for unsupported authentication plugin
82+
*/
83+
public function authenticatePacket($scramble, $authPlugin, Buffer $buffer)
7784
{
85+
if ($authPlugin !== null && $authPlugin !== 'mysql_native_password') {
86+
throw new \UnexpectedValueException('Unknown authentication plugin "' . addslashes($authPlugin) . '" requested by server');
87+
}
88+
7889
$clientFlags = Constants::CLIENT_LONG_PASSWORD |
7990
Constants::CLIENT_LONG_FLAG |
8091
Constants::CLIENT_LOCAL_FILES |
@@ -84,20 +95,28 @@ public function authenticatePacket($scramble, Buffer $buffer)
8495
Constants::CLIENT_SECURE_CONNECTION |
8596
Constants::CLIENT_CONNECT_WITH_DB;
8697

98+
if ($authPlugin !== null) {
99+
$clientFlags |= Constants::CLIENT_PLUGIN_AUTH;
100+
}
101+
87102
return pack('VVc', $clientFlags, $this->maxPacketSize, $this->charsetNumber)
88103
. "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
89104
. $this->user . "\x00"
90-
. $this->getAuthToken($scramble, $this->passwd, $buffer)
91-
. $this->dbname . "\x00";
105+
. $buffer->buildStringLen($this->authMysqlNativePassword($scramble))
106+
. $this->dbname . "\x00"
107+
. ($authPlugin !== null ? $authPlugin . "\0" : '');
92108
}
93109

94-
public function getAuthToken($scramble, $password, Buffer $buffer)
110+
/**
111+
* @param string $scramble
112+
* @return string
113+
*/
114+
private function authMysqlNativePassword($scramble)
95115
{
96-
if ($password === '') {
97-
return "\x00";
116+
if ($this->passwd === '') {
117+
return '';
98118
}
99-
$token = \sha1($scramble . \sha1($hash1 = \sha1($password, true), true), true) ^ $hash1;
100119

101-
return $buffer->buildStringLen($token);
120+
return \sha1($scramble . \sha1($hash1 = \sha1($this->passwd, true), true), true) ^ $hash1;
102121
}
103122
}

src/Io/Parser.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ class Parser
104104
*/
105105
protected $executor;
106106

107+
/**
108+
* @var ?string authentication plugin name, set if server capabilities include CLIENT_PLUGIN_AUTH
109+
* @link https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_authentication_methods.html
110+
*/
111+
private $authPlugin;
112+
107113
public function __construct(DuplexStreamInterface $stream, Executor $executor)
108114
{
109115
$this->stream = $stream;
@@ -227,7 +233,8 @@ private function parsePacket(Buffer $packet)
227233
$packet->skip(1);
228234

229235
if ($this->connectOptions['ServerCaps'] & Constants::CLIENT_PLUGIN_AUTH) {
230-
$packet->readStringNull(); // skip authentication plugin name
236+
$this->authPlugin = $packet->readStringNull();
237+
$this->debug('Authentication plugin: ' . $this->authPlugin);
231238
}
232239

233240
// init completed, continue with sending AuthenticateCommand
@@ -403,7 +410,12 @@ protected function nextRequest($isHandshake = false)
403410

404411
if ($command instanceof AuthenticateCommand) {
405412
$this->phase = self::PHASE_AUTH_SENT;
406-
$this->sendPacket($command->authenticatePacket($this->scramble, $this->buffer));
413+
try {
414+
$this->sendPacket($command->authenticatePacket($this->scramble, $this->authPlugin, $this->buffer));
415+
} catch (\UnexpectedValueException $e) {
416+
$this->onError($e);
417+
$this->stream->close();
418+
}
407419
} else {
408420
$this->seq = 0;
409421
$this->sendPacket($this->buffer->buildInt1($command->getId()) . $command->getSql());

tests/Commands/AuthenticateCommandTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPUnit\Framework\TestCase;
66
use React\Mysql\Commands\AuthenticateCommand;
7+
use React\Mysql\Io\Buffer;
78

89
class AuthenticateCommandTest extends TestCase
910
{
@@ -25,4 +26,45 @@ public function testCtorWithUnknownCharsetThrows()
2526
}
2627
new AuthenticateCommand('Alice', 'secret', '', 'utf16');
2728
}
29+
30+
public function testAuthenticatePacketWithEmptyPassword()
31+
{
32+
$command = new AuthenticateCommand('root', '', 'test', 'utf8mb4');
33+
34+
$data = $command->authenticatePacket('scramble', null, new Buffer());
35+
36+
$this->assertEquals("\x8d\xa6\0\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\0" . "test\0", $data);
37+
}
38+
39+
public function testAuthenticatePacketWithMysqlNativePasswordAuthPluginAndEmptyPassword()
40+
{
41+
$command = new AuthenticateCommand('root', '', 'test', 'utf8mb4');
42+
43+
$data = $command->authenticatePacket('scramble', 'mysql_native_password', new Buffer());
44+
45+
$this->assertEquals("\x8d\xa6\x08\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\0" . "test\0" . "mysql_native_password\0", $data);
46+
}
47+
48+
public function testAuthenticatePacketWithSecretPassword()
49+
{
50+
$command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4');
51+
52+
$data = $command->authenticatePacket('scramble', null, new Buffer());
53+
54+
$this->assertEquals("\x8d\xa6\0\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\x14\x18\xd8\x8d\x74\x77\x2c\x27\x22\x89\xe1\xcd\xbc\x4b\x5f\x77\x08\x18\x3c\x6e\xba" . "test\0", $data);
55+
}
56+
57+
public function testAuthenticatePacketWithUnknownAuthPluginThrows()
58+
{
59+
$command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4');
60+
61+
if (method_exists($this, 'expectException')) {
62+
$this->expectException('UnexpectedValueException');
63+
$this->expectExceptionMessage('Unknown authentication plugin "mysql_old_password" requested by server');
64+
} else {
65+
// legacy PHPUnit < 5.2
66+
$this->setExpectedException('UnexpectedValueException', 'Unknown authentication plugin "mysql_old_password" requested by server');
67+
}
68+
$command->authenticatePacket('scramble', 'mysql_old_password', new Buffer());
69+
}
2870
}

tests/Io/ParserTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace React\Tests\Mysql\Io;
44

5+
use React\Mysql\Commands\AuthenticateCommand;
56
use React\Mysql\Commands\QueryCommand;
67
use React\Mysql\Exception;
78
use React\Mysql\Io\Executor;
@@ -42,6 +43,23 @@ public function testClosingStreamEmitsErrorForCurrentCommand()
4243
$this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $error->getCode());
4344
}
4445

46+
public function testUnexpectedAuthPluginShouldEmitErrorOnAuthenticateCommandAndCloseStream()
47+
{
48+
$stream = new ThroughStream();
49+
$stream->on('close', $this->expectCallableOnce());
50+
51+
$command = new AuthenticateCommand('root', '', 'test', 'utf8mb4');
52+
$command->on('error', $this->expectCallableOnceWith(new \UnexpectedValueException('Unknown authentication plugin "caching_sha2_password" requested by server')));
53+
54+
$executor = new Executor();
55+
$executor->enqueue($command);
56+
57+
$parser = new Parser($stream, $executor);
58+
$parser->start();
59+
60+
$stream->write("\x49\0\0\0\x0a\x38\x2e\x34\x2e\x35\0\x5e\0\0\0\x08\x0c\x41\x44\x12\x5e\x69\x59\0\xff\xff\xff\x02\0\xff\xdf\x15\0\0\0\0\0\0\0\0\0\0\x3c\x2c\x5e\x54\x06\x04\x01\x61\x01\x20\x79\x1b\0\x63\x61\x63\x68\x69\x6e\x67\x5f\x73\x68\x61\x32\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\0");
61+
}
62+
4563
public function testUnexpectedErrorWithoutCurrentCommandWillBeIgnored()
4664
{
4765
$stream = new ThroughStream();

0 commit comments

Comments
 (0)