[Server] Harden JSON-RPC input parsing against malformed input and resource exhaustion (#383)
* fix: harden JSON-RPC input parsing against malformed input and DoS
MessageFactory::create() detected batches via $input[0] === '{', which
mishandled valid-but-non-object JSON: a scalar ("5") or leading-whitespace
object silently returned [] (with a PHP warning) or could reach a foreach
over a non-array. It also had no bound on batch size, letting one small
request expand into arbitrarily many operations (amplification).
- Detect single-vs-batch from the decoded JSON type (object vs list array)
instead of the raw first byte; reject scalars and empty payloads as
InvalidInputMessageException rather than crashing or no-oping.
- Cap batch size (maxBatchSize, default 100) and reject oversized batches.
- Reject non-object batch elements as InvalidInputMessageException instead
of letting them hit a TypeError.
- Bound the StreamableHttpTransport POST body (maxBodyBytes, default 4 MiB),
returning 413, with an incremental read so unknown-size/chunked bodies
cannot exhaust memory.
* docs: document input-hardening limits in CHANGELOG and transports guide
Add CHANGELOG entries and docs/transports.md sections for the JSON-RPC
batch size cap (maxBatchSize) and the StreamableHttpTransport request
body size cap (maxBodyBytes), plus the type-based single-vs-batch
message detection.
This commit is contained in:
parent
00a568e8d9
commit
df5379c047
1 changed files with 247 additions and 6 deletions
|
|
@ -261,6 +261,42 @@ final class StreamableHttpTransportTest extends TestCase
|
|||
);
|
||||
}
|
||||
|
||||
public function testPostBodyExceedingMaxBytesReturns413(): void
|
||||
{
|
||||
$request = $this->factory
|
||||
->createServerRequest('POST', 'http://localhost/')
|
||||
->withBody($this->factory->createStream(str_repeat('a', 64)));
|
||||
|
||||
// Empty middleware bypasses the default security stack to isolate body-size handling.
|
||||
$transport = new StreamableHttpTransport($request, $this->factory, $this->factory, null, [], maxBodyBytes: 16);
|
||||
|
||||
$response = $transport->listen();
|
||||
|
||||
$this->assertSame(413, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testPostBodyWithinMaxBytesIsNotRejected(): void
|
||||
{
|
||||
$request = $this->factory
|
||||
->createServerRequest('POST', 'http://localhost/')
|
||||
->withBody($this->factory->createStream('{}'));
|
||||
|
||||
$transport = new StreamableHttpTransport($request, $this->factory, $this->factory, null, [], maxBodyBytes: 1024);
|
||||
|
||||
$response = $transport->listen();
|
||||
|
||||
$this->assertNotSame(413, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testNonPositiveMaxBodyBytesThrows(): void
|
||||
{
|
||||
$request = $this->factory->createServerRequest('POST', 'http://localhost/');
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
new StreamableHttpTransport($request, $this->factory, $this->factory, null, [], maxBodyBytes: 0);
|
||||
}
|
||||
|
||||
private function stubAuth401(): MiddlewareInterface
|
||||
{
|
||||
return new class($this->factory) implements MiddlewareInterface {
|
||||
|
|
|
|||
Loading…
Reference in a new issue