Security Advisories (1)
CVE-2026-104380 (2026-10-06)

Punk versions from 0.48 before 0.55 for Perl route Extended CONNECT requests to any GET route without an Origin check in ps_serve_one. On HTTP/2 and HTTP/3 a WebSocket handshake arrives as an Extended CONNECT, which is matched as a GET and so reaches every GET route, API operation and mount. The Origin check runs only when a websocket route matches. On this transport the handler's status is the handshake response, and a 2xx accepts it. A cross-origin page can open a WebSocket to any path and learn from its open or error event whether that path returns 2xx.

NAME

Punk::WebSocket::Room - pub/sub groups of WebSocket connections

SYNOPSIS

use Punk::WebSocket::Room;

sub join_chat {
    my ($c, $ws) = @_;
    my $room = Punk::WebSocket::Room->named('lobby');

    $ws->on(open    => sub { $room->join($_[0]) });
    $ws->on(message => sub {
        my ($ws, $text) = @_;
        $room->broadcast($text, $ws);      # everyone except the sender
    });
    $ws->on(close   => sub { $room->leave($_[0]) });
}

DESCRIPTION

A named group of connections, with one encode per broadcast: the frame is built once and the same bytes are queued to every member.

Membership is held weakly and pruned on every access, so a connection that goes away leaves its rooms by itself - calling "leave" from a close handler is tidy but not required.

A broadcast reaches the whole pool. Each worker holds only the connections it accepted, so a broadcast is published on Hyperman's cross-worker message bus and every worker fans the frame out to its own members. The frame is encoded once, by the worker that broadcast it, and the same bytes travel - so a room of a thousand people across four workers does one encode, not four.

There is one delivery path. The publishing worker does not also send locally; it receives its own publication like everybody else. Sending locally and publishing would deliver twice to the members in front of you, and would leave two paths to drift apart - so a bug in the shared one would be invisible to anyone testing on a single worker.

Where there is no pool - a server that is not Hyperman, a Hyperman older than 0.28, Windows, or a compiler without the atomics the shared ring needs - a room is local, which is what it always was. It does not pretend otherwise.

The count a broadcast returns is what this worker delivered. A total across the pool is not knowable at the moment of the call without waiting for it, and a plausible number that is not the truth is precisely the fault this replaced.

METHODS

named($name)

The worker's room of that name, created on first use.

join($ws) / leave($ws) / has($ws)

clients

The live members, as a list. List context only: like any list-returning XS call, scalar $room->clients yields the last member, not a count - that is what count is for.

count

How many.

broadcast($text, $except?)

broadcast_binary($bytes, $except?)

Send to every open member, optionally skipping one connection (usually the sender). Returns the number sent.

close_all($code = 1000, $reason = '')

Close every member and empty the room.

clear

Empty the room without closing anything.

AUTHOR

LNATION <email@lnation.org>

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)