Feb 17, 2026·6 min read·1 visit
A critical logic flaw in Pterodactyl Panel < 1.12.1 allowed SFTP sessions to persist after user account deletion or password resets. Because the Panel failed to signal the Wings daemon to terminate active connections, a banned user with an open SFTP client could continue to read, write, or delete files indefinitely. The fix involves a new asynchronous revocation job that forcibly kills these sessions.
In the world of game server hosting, Pterodactyl is the undisputed king. But a synchronization gap between the management Panel and the remote Wings daemon created a zombie apocalypse scenario: SFTP sessions that refused to die. This vulnerability allowed malicious users to maintain full filesystem access to servers even after their accounts were deleted or passwords changed, turning a standard termination procedure into a race against a lingering, unauthorized open socket.
To understand why this bug exists, you have to understand Pterodactyl's split-personality architecture. You have the Panel, a PHP application that acts as the brain. It handles user management, billing logic, and the pretty UI. Then you have Wings, the Go-based daemon sitting on the remote servers. Wings is the brawn; it runs the Docker containers, manages the file systems, and handles the heavy lifting.
Communication between these two is vital. When a user logs in to SFTP, they aren't authenticating against a local Linux user file; they are authenticating against Wings, which checks with the Panel to see if the credentials are valid. It's a classic distributed system pattern.
The problem with distributed systems is state. Who owns the truth? In this case, the Panel knew the user was banned. The database knew the user was banned. But Wings? Wings was still happily piping data through an open TCP socket, completely unaware that the entity on the other end had been digitally executed minutes ago.
The vulnerability wasn't a buffer overflow or a complex cryptographic failure. It was a failure of courtesy. When an administrator deletes a user or a user resets their password, the Panel updates its database immediately. Any new login attempts would fail instantly because Wings would ask the Panel, "Hey, is this guy cool?" and the Panel would say, "New phone, who dis?"
However, for existing connections, silence reigned supreme. SFTP (SSH File Transfer Protocol) runs over a persistent connection. Once the handshake is done and the session is established, the authentication phase is over. Unless the software explicitly implements a "kill switch" or periodic re-validation, that pipe stays open until the client disconnects or the network drops.
Pterodactyl simply didn't have code in place to tap Wings on the shoulder and say, "Cut the cord." This meant a user could open an SFTP client, initiate a transfer or just idle, have their account deleted by an admin, and then proceed to rm -rf / the server contents hours later. It is the digital equivalent of firing an employee but forgetting to deactivate their keycard while they are still inside the building.
The fix, landed in version 1.12.1 via commit 0e74f3aadec89405751ec602c77fc1d030a417c0, introduces the missing communication loop. The developers realized that database state changes needed to trigger active enforcement on the edge nodes.
The patch introduces a new event listener infrastructure. Specifically, they added a RevokeSftpAccessJob. This isn't just a simple API call; it's a queued job with retries. This is crucial because if the remote node is temporarily offline when the user is banned, a synchronous call would fail, and the zombie session would survive. By using a job queue with exponential backoff, the Panel ensures that the "kill" command is eventually delivered.
Here is the logic added to the RevocationListener.php. Notice how it grabs every node the user had access to and dispatches a job to hunt them down:
// app/Listeners/RevocationListener.php
public function revoke(Deleting|PasswordChanged $event): void
{
$user = $event->user;
// Find all nodes where this user has a server
Node::query()
->whereIn('nodes.id', $user->accessibleServers()->select('servers.node_id')->distinct())
->chunk(50, function (Collection $nodes) use ($user) {
// Dispatch the hit squad (RevokeSftpAccessJob)
$nodes->each(fn (Node $node) => RevokeSftpAccessJob::dispatch($user->uuid, $node));
});
}This code effectively says: "I don't care where you are hiding. I will find every server you have access to, and I will terminate your connections."
Let's roleplay a disgruntled community manager, "Dave". Dave knows he's about to be fired from the gaming community he helps run. Dave is petty. Dave wants to watch the world burn.
Step 1: The Setup Dave logs into his FileZilla client and connects to the Minecraft server's SFTP interface. He navigates to the backup folder. He doesn't do anything yet; he just keeps the connection alive (most clients send keep-alive packets automatically).
Step 2: The Ban The Head Admin bans Dave. They delete his Pterodactyl account to be safe. They pat themselves on the back for a job well done. In the Panel logs, Dave is gone. In the database, Dave is gone.
Step 3: The Ghost
Dave's FileZilla client doesn't disconnect. The TCP stream is still valid. The Wings daemon hasn't received a RST packet or a termination signal. Dave, now technically a non-existent user, highlights the entire world_the_end folder and hits delete.
Step 4: The Damage
Because the session holds the file descriptors and permissions of the user at the time of connection, the deletion proceeds. Dave then uploads a small script named start.sh that loops forever and eats RAM, crashing the node. Dave closes FileZilla and goes to lunch. The admins are left wondering how a "deleted" user just nuked their infrastructure.
If you are running Pterodactyl Panel versions prior to 1.12.1, you are vulnerable. The update path is straightforward and standard for Laravel applications. Pull the new code, run the migrations, and restart the queue workers so they pick up the new job definitions.
php artisan down
git pull origin stable
composer install --no-dev --optimize-autoloader
php artisan migrate
php artisan queue:restart
php artisan upIf you cannot update immediately (perhaps you have custom modifications that make merging painful), your mitigation strategy is brute force. Whenever you demote a high-privilege user or delete a suspicious account, you must manually restart the wings service on the nodes they accessed.
systemctl restart wings
This will sever all active connections to that node. It's a sledgehammer, not a scalpel, but it guarantees that the ghost sessions are exorcised. Monitoring tools can also help; keeping an eye on netstat for long-lived connections from known bad IPs is good practice, though difficult to attribute to specific users without deep packet inspection or log correlation.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Pterodactyl Panel Pterodactyl | < 1.12.1 | 1.12.1 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | Improper Session Handling / Auth Bypass |
| Attack Vector | Network |
| Confidentiality | High (File Access) |
| Integrity | High (File Modification) |
| Availability | High (Service Disruption via file deletion) |
| CVSS Score | 7.5 (High) |
The software does not properly terminate a session when the user's account is deleted or privileges are revoked, allowing continued access.