Skip to content

Commit 9223131

Browse files
committed
Limit SSH client STDERR line buffer to 64 KiB
1 parent acd48d8 commit 9223131

6 files changed

Lines changed: 98 additions & 1 deletion

‎src/Io/LineSeparatedReader.php‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class LineSeparatedReader extends EventEmitter implements ReadableStreamInterfac
1313
private $stream;
1414
private $buffer = '';
1515
private $closed = false;
16+
private $maxlength = 65536;
1617

1718
public function __construct(ReadableStreamInterface $stream)
1819
{
@@ -53,12 +54,16 @@ public function handleData($chunk)
5354
{
5455
$this->buffer .= $chunk;
5556

56-
while (($pos = strpos($this->buffer, "\n")) !== false) {
57+
while (($pos = strpos($this->buffer, "\n")) !== false && $pos <= $this->maxlength) {
5758
$chunk = rtrim(substr($this->buffer, 0, $pos), "\r\n");
5859
$this->buffer = (string)substr($this->buffer, $pos + 1);
5960

6061
$this->emit('data', array($chunk));
6162
}
63+
64+
if (isset($this->buffer[$this->maxlength])) {
65+
$this->handleError(new \OverflowException('Buffer size exceeded'));
66+
}
6267
}
6368

6469
public function handleEnd()

‎src/SshProcessConnector.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ public function connect($uri)
138138
$connected = true;
139139
});
140140

141+
// terminate process if STDERR fails, e.g. because of excessive line length
142+
$stderr->on('error', function (\Exception $e) use ($process, &$last) {
143+
$last = $e->getMessage();
144+
$process->terminate();
145+
});
146+
141147
// If STDERR closes before connection was established, explicitly close STDOUT stream.
142148
// The STDOUT stream starts in a paused state and as such will prevent the process exit
143149
// logic from triggering when it is not resumed.

‎src/SshSocksConnector.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ private function listen()
243243
}
244244
});
245245

246+
// terminate process if STDERR fails, e.g. because of excessive line length
247+
$stderr->on('error', function (\Exception $e) use ($process, &$last) {
248+
$last = $e->getMessage();
249+
$process->terminate();
250+
});
251+
246252
$process->on('exit', function ($code) use ($deferred, &$last) {
247253
$deferred->reject(new \RuntimeException(
248254
$last,

‎tests/IntegrationSshProcessConnectorTest.php‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,22 @@ public function testConnectCancellationWillRejectWithException()
7474
Loop::run();
7575
}
7676

77+
public function testConnectWillRejectWithExceptionAndTerminateProcessWhenProcessOutputsLineExceedingMaximumBufferSize()
78+
{
79+
$connector = new SshProcessConnector('host');
80+
81+
$ref = new \ReflectionProperty($connector, 'cmd');
82+
if (PHP_VERSION_ID < 80100) {
83+
$ref->setAccessible(true);
84+
}
85+
$ref->setValue($connector, 'head -c 70000 /dev/zero | tr "\0" a >&2; exec sleep 10; #');
86+
87+
$promise = $connector->connect('example.com:80');
88+
89+
$this->setExpectedException('RuntimeException', 'Connection to example.com:80 failed because SSH client died (Buffer size exceeded)');
90+
\React\Async\await($promise);
91+
}
92+
7793
public function testConnectWillResolveWithConnectionThatWillEmitImmediateDataFromProcessStdoutAfterChannelOpenConfirmMessage()
7894
{
7995
$connector = new SshProcessConnector('host');

‎tests/IntegrationSshSocksConnectorTest.php‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,20 @@ public function testConnectWillRejectWithExceptionAndTerminateIdleProcessWhenSoc
117117
$this->assertInstanceOf('React\ChildProcess\Process', $process);
118118
$this->assertTrue($process->isTerminated());
119119
}
120+
121+
public function testConnectWillRejectWithExceptionAndTerminateProcessWhenProcessOutputsLineExceedingMaximumBufferSize()
122+
{
123+
$connector = new SshSocksConnector('host');
124+
125+
$ref = new \ReflectionProperty($connector, 'cmd');
126+
if (PHP_VERSION_ID < 80100) {
127+
$ref->setAccessible(true);
128+
}
129+
$ref->setValue($connector, 'head -c 70000 /dev/zero | tr "\0" a >&2; exec sleep 10; #');
130+
131+
$promise = $connector->connect('example.com:80');
132+
133+
$this->setExpectedException('RuntimeException', 'Connection to example.com:80 failed because SSH client process died (Buffer size exceeded)');
134+
\React\Async\await($promise);
135+
}
120136
}

‎tests/Io/LineSeparatedReaderTest.php‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,54 @@ public function testStreamDataWillEmitDataBehindNewlineWhenDataContainsDataBehin
3636
$input->write("hello!\nignored");
3737
}
3838

39+
public function testStreamDataWillEmitDataWhenDataContainsNewlineAtMaximumBufferSize()
40+
{
41+
$input = new ThroughStream();
42+
$stream = new LineSeparatedReader($input);
43+
44+
$stream->on('data', $this->expectCallableOnceWith(str_repeat('a', 65536)));
45+
$stream->on('error', $this->expectCallableNever());
46+
47+
$input->write(str_repeat('a', 65536));
48+
$input->write("\n");
49+
}
50+
51+
public function testStreamDataWillEmitErrorAndCloseWhenDataExceedsMaximumBufferSize()
52+
{
53+
$input = new ThroughStream();
54+
$stream = new LineSeparatedReader($input);
55+
56+
$stream->on('data', $this->expectCallableNever());
57+
$stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('OverflowException')));
58+
$stream->on('close', $this->expectCallableOnce());
59+
60+
$input->write(str_repeat('a', 40000));
61+
$input->write(str_repeat('a', 40000));
62+
}
63+
64+
public function testStreamDataWillEmitErrorAndCloseWhenDataContainsNewlineBehindMaximumBufferSize()
65+
{
66+
$input = new ThroughStream();
67+
$stream = new LineSeparatedReader($input);
68+
69+
$stream->on('data', $this->expectCallableNever());
70+
$stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('OverflowException')));
71+
$stream->on('close', $this->expectCallableOnce());
72+
73+
$input->write(str_repeat('a', 65537) . "\n");
74+
}
75+
76+
public function testStreamDataWillEmitDataAndErrorWhenDataBehindNewlineExceedsMaximumBufferSize()
77+
{
78+
$input = new ThroughStream();
79+
$stream = new LineSeparatedReader($input);
80+
81+
$stream->on('data', $this->expectCallableOnceWith("hello!"));
82+
$stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('OverflowException')));
83+
84+
$input->write("hello!\n" . str_repeat('a', 65537));
85+
}
86+
3987

4088
public function testStreamEndWillEndWithoutDataWhenNoPreviousDataInBuffer()
4189
{

0 commit comments

Comments
 (0)