VYPR
Medium severityNVD Advisory· Published Oct 31, 2024· Updated Apr 15, 2026

CVE-2024-50347

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.

PackageAffected versionsPatched versions
laravel/reverbPackagist
< 1.4.01.4.0

Patches

2
73cc140d76e8

[1.x] Implements API signature verification (#252)

https://github.com/laravel/reverbJoe DixonSep 25, 2024via ghsa
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);
         }
     }
    

Vulnerability 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

News mentions

0

No linked articles in our index yet.