CVE-2024-50347
Description
Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications. Prior to 1.4.0, there is an issue where verification signatures for requests sent to Reverb's Pusher-compatible API were not being verified. This API is used in scenarios such as broadcasting a message from a backend service or for obtaining statistical information (such as number of connections) about a given channel. This issue only affects the Pusher-compatible API endpoints and not the WebSocket connections themselves. In order to exploit this vulnerability, the application ID which, should never be exposed, would need to be known by an attacker. This vulnerability is fixed in 1.4.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
laravel/reverbPackagist | < 1.4.0 | 1.4.0 |
Patches
273cc140d76e8[1.x] Implements API signature verification (#252)
10 files changed · +87 −39
src/Protocols/Pusher/Http/Controllers/Controller.php+3 −1 modified@@ -48,6 +48,7 @@ public function verify(RequestInterface $request, Connection $connection, $appId $this->setApplication($appId); $this->setChannels(); + $this->verifySignature($request); } /** @@ -100,8 +101,9 @@ protected function verifySignature(RequestInterface $request): void ]); $signature = hash_hmac('sha256', $signature, $this->application->secret()); + $authSignature = $this->query['auth_signature'] ?? ''; - if ($signature !== $this->query['auth_signature']) { + if ($signature !== $authSignature) { throw new HttpException(401, 'Authentication signature invalid.'); } }
tests/Feature/Protocols/Pusher/Reverb/ChannelControllerTest.php+7 −0 modified@@ -1,6 +1,7 @@ <?php use Laravel\Reverb\Tests\ReverbTestCase; +use React\Http\Message\ResponseException; use function React\Async\await; @@ -141,3 +142,9 @@ expect($response->getHeader('Content-Length'))->toBe(['40']); }); + +it('fails when using an invalid signature', function () { + $response = await($this->request('channels/test-channel-one?info=user_count,subscription_count,cache')); + + expect($response->getStatusCode())->toBe(401); +})->throws(ResponseException::class, exceptionCode: 401);
tests/Feature/Protocols/Pusher/Reverb/ChannelsControllerTest.php+7 −0 modified@@ -2,6 +2,7 @@ use Illuminate\Support\Arr; use Laravel\Reverb\Tests\ReverbTestCase; +use React\Http\Message\ResponseException; use function React\Async\await; @@ -122,3 +123,9 @@ expect($response->getHeader('Content-Length'))->toBe(['81']); }); + +it('fails when using an invalid signature', function () { + $response = await($this->request('channels?info=user_count')); + + expect($response->getStatusCode())->toBe(401); +})->throws(ResponseException::class, exceptionCode: 401);
tests/Feature/Protocols/Pusher/Reverb/ChannelUsersControllerTest.php+10 −4 modified@@ -13,11 +13,11 @@ it('returns an error when presence channel not provided', function () { subscribe('test-channel'); await($this->signedRequest('channels/test-channel/users')); -})->throws(ResponseException::class); +})->throws(ResponseException::class, exceptionCode: 400); it('returns an error when unoccupied channel provided', function () { await($this->signedRequest('channels/presence-test-channel/users')); -})->throws(ResponseException::class); +})->throws(ResponseException::class, exceptionCode: 404); it('returns the user data', function () { $channel = app(ChannelManager::class) @@ -53,13 +53,13 @@ subscribe('test-channel'); await($this->signedRequest('channels/test-channel/users')); -})->throws(ResponseException::class); +})->throws(ResponseException::class, exceptionCode: 400); it('returns an error when gathering unoccupied channel provided', function () { $this->usingRedis(); await($this->signedRequest('channels/presence-test-channel/users')); -})->throws(ResponseException::class); +})->throws(ResponseException::class, exceptionCode: 404); it('can send the content-length header', function () { $channel = app(ChannelManager::class) @@ -120,3 +120,9 @@ expect($response->getHeader('Content-Length'))->toBe(['38']); }); + +it('fails when using an invalid signature', function () { + $response = await($this->request('channels/presence-test-channel/users')); + + expect($response->getStatusCode())->toBe(401); +})->throws(ResponseException::class, exceptionCode: 401);
tests/Feature/Protocols/Pusher/Reverb/ConnectionsControllerTest.php+7 −0 modified@@ -1,6 +1,7 @@ <?php use Laravel\Reverb\Tests\ReverbTestCase; +use React\Http\Message\ResponseException; use function React\Async\await; @@ -71,3 +72,9 @@ expect($response->getHeader('Content-Length'))->toBe(['17']); }); + +it('fails when using an invalid signature', function () { + $response = await($this->request('connections')); + + expect($response->getStatusCode())->toBe(401); +})->throws(ResponseException::class, exceptionCode: 401);
tests/Feature/Protocols/Pusher/Reverb/EventsBatchControllerTest.php+12 −0 modified@@ -171,3 +171,15 @@ expect($response->getHeader('Content-Length'))->toBe(['12']); }); + +it('fails when using an invalid signature', function () { + $response = await($this->postRequest('batch_events', ['batch' => [ + [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + 'data' => json_encode(['some' => 'data']), + ], + ]])); + + expect($response->getStatusCode())->toBe(401); +})->throws(ResponseException::class, exceptionCode: 401);
tests/Feature/Protocols/Pusher/Reverb/EventsControllerTest.php+11 −1 modified@@ -182,7 +182,7 @@ 'name' => 'NewEvent', 'channel' => 'test-channel', 'data' => json_encode([str_repeat('a', 10_100)]), - ], appId: '654321')); + ], appId: '654321', key: 'reverb-key-2', secret: 'reverb-secret-2')); expect($response->getStatusCode())->toBe(200); expect($response->getBody()->getContents())->toBe('{}'); @@ -207,3 +207,13 @@ expect($response->getHeader('Content-Length'))->toBe(['2']); }); + +it('fails when using an invalid signature', function () { + $response = await($this->postRequest('events', [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + 'data' => json_encode(['some' => 'data']), + ])); + + expect($response->getStatusCode())->toBe(401); +})->throws(ResponseException::class, exceptionCode: 401);
tests/Feature/Protocols/Pusher/Reverb/ServerTest.php+1 −1 modified@@ -465,7 +465,7 @@ 'name' => 'NewEvent', 'channel' => 'test-channel', 'data' => json_encode([str_repeat('a', 150_000)]), - ], appId: '654321')); + ], appId: '654321', key: 'reverb-key-2', secret: 'reverb-secret-2')); expect($response->getStatusCode())->toBe(200); expect($response->getBody()->getContents())->toBe('{}');
tests/Feature/Protocols/Pusher/Reverb/UsersTerminateControllerTest.php+7 −1 modified@@ -9,7 +9,7 @@ it('returns an error when connection cannot be found', function () { await($this->signedPostRequest('channels/users/not-a-user/terminate_connections')); -})->throws(ResponseException::class); +})->throws(ResponseException::class, exceptionCode: 404); it('unsubscribes from all channels and terminates a user', function () { $connection = connect(); @@ -54,3 +54,9 @@ expect(collect(channels()->all())->get('test-channel-two')->connections())->toHaveCount(1); expect($response->getHeader('Content-Length'))->toBe(['2']); }); + +it('fails when using an invalid signature', function () { + $response = await($this->postRequest('users/987/terminate_connections')); + + expect($response->getStatusCode())->toBe(401); +})->throws(ResponseException::class, exceptionCode: 401);
tests/ReverbTestCase.php+22 −31 modified@@ -160,16 +160,29 @@ public function requestWithoutAppId(string $path, string $method = 'GET', mixed /** * Send a signed request to the server. */ - public function signedRequest(string $path, string $method = 'GET', mixed $data = '', string $host = '0.0.0.0', string $port = '8080', string $appId = '123456'): PromiseInterface + public function signedRequest(string $path, string $method = 'GET', mixed $data = '', string $host = '0.0.0.0', string $port = '8080', string $appId = '123456', string $key = 'reverb-key', string $secret = 'reverb-secret'): PromiseInterface { - $hash = md5(json_encode($data)); $timestamp = time(); - $query = "auth_key=reverb-key&auth_timestamp={$timestamp}&auth_version=1.0&body_md5={$hash}"; - $string = "POST\n/apps/{$appId}/{$path}\n$query"; - $signature = hash_hmac('sha256', $string, 'reverb-secret'); - $path = Str::contains($path, '?') ? "{$path}&{$query}" : "{$path}?{$query}"; - return $this->request("{$path}&auth_signature={$signature}", $method, $data, $host, $port, $appId); + $query = Str::contains($path, '?') ? Str::after($path, '?') : ''; + $auth = "auth_key={$key}&auth_timestamp={$timestamp}&auth_version=1.0"; + $query = $query ? "{$query}&{$auth}" : $auth; + + $query = explode('&', $query); + sort($query); + $query = implode('&', $query); + + $path = Str::before($path, '?'); + + if ($data) { + $hash = md5(json_encode($data)); + $query .= "&body_md5={$hash}"; + } + + $string = "{$method}\n/apps/{$appId}/{$path}\n$query"; + $signature = hash_hmac('sha256', $string, $secret); + + return $this->request("{$path}?{$query}&auth_signature={$signature}", $method, $data, $host, $port, $appId); } /** @@ -183,30 +196,8 @@ public function postRequest(string $path, ?array $data = [], string $host = '0.0 /** * Send a signed POST request to the server. */ - public function signedPostRequest(string $path, ?array $data = [], string $host = '0.0.0.0', string $port = '8080', string $appId = '123456'): PromiseInterface + public function signedPostRequest(string $path, ?array $data = [], string $host = '0.0.0.0', string $port = '8080', string $appId = '123456', $key = 'reverb-key', $secret = 'reverb-secret'): PromiseInterface { - $hash = md5(json_encode($data)); - $timestamp = time(); - $query = "auth_key=reverb-key&auth_timestamp={$timestamp}&auth_version=1.0&body_md5={$hash}"; - $string = "POST\n/apps/{$appId}/{$path}\n$query"; - $signature = hash_hmac('sha256', $string, 'reverb-secret'); - - return $this->postRequest("{$path}?{$query}&auth_signature={$signature}", $data, $host, $port, $appId); - } - - /** - * Send a signed GET request to the server. - */ - public function getWithSignature(string $path, array $data = [], string $host = '0.0.0.0', string $port = '8080', string $appId = '123456'): PromiseInterface - { - $hash = md5(json_encode($data)); - $timestamp = time(); - $query = "auth_key=reverb-key&auth_timestamp={$timestamp}&auth_version=1.0&body_md5={$hash}"; - $string = "POST\n/apps/{$appId}/{$path}\n$query"; - $signature = hash_hmac('sha256', $string, 'reverb-secret'); - - $path = Str::contains($path, '?') ? "{$path}&{$query}" : "{$path}?{$query}"; - - return $this->request("{$path}&auth_signature={$signature}", 'GET', '', $host, $port, $appId); + return $this->signedRequest($path, 'POST', $data, $host, $port, $appId, $key, $secret); } }
6cc4f3909099Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-pfrr-xvrf-pxjxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-50347ghsaADVISORY
- github.com/laravel/reverb/commit/73cc140d76e803b151fc2dd2e4eb3eb784a82ee2nvdWEB
- github.com/laravel/reverb/pull/252nvdWEB
- github.com/laravel/reverb/releases/tag/v1.4.0nvdWEB
- github.com/laravel/reverb/security/advisories/GHSA-pfrr-xvrf-pxjxnvdWEB
News mentions
0No linked articles in our index yet.