Skip to content

Commit e75200b

Browse files
committed
Signed queries don't break the request for other middlewares
`$request->getBody()->getContents()` consumes the body without rewinding. Repeated calls, by other middlewares, will return empty string. So other middlewares will think the whole request is empty, and GraphQL will throw. To avoid this, we should rewind the stream. However, not all streams are rewindable and would throw exceptions. To avoid that, we return a new request identical to original with a brand-new stream for body.
1 parent 6c8847f commit e75200b

File tree

2 files changed

+32
-13
lines changed

2 files changed

+32
-13
lines changed

src/Middleware/SignedQueryMiddleware.php

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Cake\Chronos\Chronos;
88
use Ecodev\Felix\Validator\IPRange;
99
use Exception;
10+
use Laminas\Diactoros\CallbackStream;
1011
use Psr\Http\Message\ResponseInterface;
1112
use Psr\Http\Message\ServerRequestInterface;
1213
use Psr\Http\Server\MiddlewareInterface;
@@ -44,18 +45,18 @@ public function __construct(
4445
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
4546
{
4647
if ($this->required) {
47-
$this->verify($request);
48+
$request = $this->verify($request);
4849
}
4950

5051
return $handler->handle($request);
5152
}
5253

53-
private function verify(ServerRequestInterface $request): void
54+
private function verify(ServerRequestInterface $request): ServerRequestInterface
5455
{
5556
$signature = $request->getHeader('X-Signature')[0] ?? '';
5657
if (!$signature) {
5758
if ($this->isAllowedIp($request)) {
58-
return;
59+
return $request;
5960
}
6061

6162
throw new Exception('Missing `X-Signature` HTTP header in signed query', 403);
@@ -66,10 +67,11 @@ private function verify(ServerRequestInterface $request): void
6667
$hash = $m['hash'];
6768

6869
$this->verifyTimestamp($timestamp);
69-
$this->verifyHash($request, $timestamp, $hash);
70-
} else {
71-
throw new Exception('Invalid `X-Signature` HTTP header in signed query', 403);
70+
71+
return $this->verifyHash($request, $timestamp, $hash);
7272
}
73+
74+
throw new Exception('Invalid `X-Signature` HTTP header in signed query', 403);
7375
}
7476

7577
private function verifyTimestamp(string $timestamp): void
@@ -83,33 +85,45 @@ private function verifyTimestamp(string $timestamp): void
8385
}
8486
}
8587

86-
private function verifyHash(ServerRequestInterface $request, string $timestamp, string $hash): void
88+
private function verifyHash(ServerRequestInterface $request, string $timestamp, string $hash): ServerRequestInterface
8789
{
88-
$operations = $this->getOperations($request);
90+
['request' => $request, 'operations' => $operations] = $this->getOperations($request);
8991
$payload = $timestamp . $operations;
9092

9193
foreach ($this->keys as $key) {
9294
$computedHash = hash_hmac('sha256', $payload, $key);
9395
if ($hash === $computedHash) {
94-
return;
96+
return $request;
9597
}
9698
}
9799

98100
throw new Exception('Invalid signed query', 403);
99101
}
100102

101-
private function getOperations(ServerRequestInterface $request): mixed
103+
/**
104+
* @return array{request: ServerRequestInterface, operations: string}
105+
*/
106+
private function getOperations(ServerRequestInterface $request): array
102107
{
103108
$contents = $request->getBody()->getContents();
109+
104110
if ($contents) {
105-
return $contents;
111+
return [
112+
// Pseudo-rewind the request, even if non-rewindable, so the next
113+
// middleware still accesses the stream from the beginning
114+
'request' => $request->withBody(new CallbackStream(fn () => $contents)),
115+
'operations' => $contents,
116+
];
106117
}
107118

108119
$parsedBody = $request->getParsedBody();
109120
if (is_array($parsedBody)) {
110121
$operations = $parsedBody['operations'] ?? null;
111122
if ($operations) {
112-
return $operations;
123+
return [
124+
'request' => $request,
125+
'operations' => $operations,
126+
];
113127
}
114128
}
115129

tests/Middleware/SignedQueryMiddlewareTest.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Laminas\Diactoros\Response;
1111
use Laminas\Diactoros\ServerRequest;
1212
use PHPUnit\Framework\TestCase;
13+
use Psr\Http\Message\ServerRequestInterface;
1314
use Psr\Http\Server\RequestHandlerInterface;
1415

1516
class SignedQueryMiddlewareTest extends TestCase
@@ -59,7 +60,11 @@ private function process(array $keys, bool $required, string $ip, string $body,
5960
$handler = $this->createMock(RequestHandlerInterface::class);
6061
$handler->expects($expectExceptionMessage ? self::never() : self::once())
6162
->method('handle')
62-
->willReturn(new Response());
63+
->willReturnCallback(function (ServerRequestInterface $incomingRequest) use ($body) {
64+
self::assertSame($body, $incomingRequest->getBody()->getContents(), 'the original body content is still available for next middlewares');
65+
66+
return new Response();
67+
});
6368

6469
$middleware = new SignedQueryMiddleware($keys, ['1.2.3.4', '2a01:198:603:0::/65'], $required);
6570

0 commit comments

Comments
 (0)