seatOf in src/socket.ts gives an anonymous socket anon:<12 random bytes> and says why in its own comment: reading a guest's cookie is src/session.ts, "which is not merged yet". It merged first — #6 landed as b98061e, the socket as ea28cb9 on top of it — so the comment is stale and the hole it describes is open on main today.
What that means, against the record #57 just wrote: a guest is a fresh seat per connection, so one key holds one seat is not true for anybody who is not signed in; seatHolder never matches a guest, so the second tab is a second swimmer rather than a second window on one mote; and a client with no cookie jar — the exact attacker #49 was opened about — takes a seat per socket, bounded only by the 256-socket shared cap. #8 is the issue that writes join, and it will read key off the socket and believe it.
Done looks like
[ ] seatOf uses the player key the record names: playerKey(request, who) from src/session.ts — the forge id for someone signed in, the guest id derived from the cookie for everybody else. The stale comment goes with it.
[ ] A request carrying no session gets no seat, as the record decides, and is admitted as a spectator: SocketData says which it is, the socket opens, frames still go down it, and steering from it is dropped the way an unadmitted steer already is. Nobody is turned away and nothing is refused silently.
[ ] The spectator is told, in the sentence the record already wrote and src/concept.ts already exports (SPECTATOR), over the socket's existing message shape — not invented here a second time.
[ ] Two sockets under one key are two windows on one seat: seatHolder(key) finds the same seat for both, and a second connection is neither refused nor given its own. How many a key may hold open stays the per-key ration that is already there.
[ ] Signing in changes the key: the record says the guest's seat is released in the same tick the forge id takes one. The socket half of that is in this issue — the old socket's key stops being a seat holder — and the mote's inheritance stays #8's.
[ ] src/socket.test.ts grows the cases that would have caught this: two guest sockets carrying the same cookie hold one seat; two guest sockets with different cookies hold two; a socket with no cookie at all holds none and is a spectator; a signed-in socket's key is the forge id regardless of the cookie it also carries.
[ ] The README's socket paragraph says what a seat is keyed by, in one sentence, rather than leaving the code as the only statement of it.
RisksplayerKey returns nothing when there is no session, and that path is the whole point — resist minting a key to make a type simpler, because minting is what this issue removes. A guest id is derived from a cookie the app set HttpOnly; it must never be read from a query string, a header a client chooses or a message the socket sends. And the upgrade has no response to set a cookie on, so the first visit is a page load then a socket — one round trip, which is the record's own answer and is worth restating in the pull.
Fits Vesper's brief, with Sentinel on the pull: this is an authorization bug on the door that admits people to the world.
seatOfinsrc/socket.tsgives an anonymous socketanon:<12 random bytes>and says why in its own comment: reading a guest's cookie issrc/session.ts, "which is not merged yet". It merged first — #6 landed as b98061e, the socket as ea28cb9 on top of it — so the comment is stale and the hole it describes is open onmaintoday.What that means, against the record #57 just wrote: a guest is a fresh seat per connection, so one key holds one seat is not true for anybody who is not signed in;
seatHoldernever matches a guest, so the second tab is a second swimmer rather than a second window on one mote; and a client with no cookie jar — the exact attacker #49 was opened about — takes a seat per socket, bounded only by the 256-socket shared cap. #8 is the issue that writesjoin, and it will readkeyoff the socket and believe it.Done looks like
seatOfuses the player key the record names:playerKey(request, who)fromsrc/session.ts— the forge id for someone signed in, the guest id derived from the cookie for everybody else. The stale comment goes with it.SocketDatasays which it is, the socket opens, frames still go down it, and steering from it is dropped the way an unadmitted steer already is. Nobody is turned away and nothing is refused silently.src/concept.tsalready exports (SPECTATOR), over the socket's existing message shape — not invented here a second time.seatHolder(key)finds the same seat for both, and a second connection is neither refused nor given its own. How many a key may hold open stays the per-key ration that is already there.src/socket.test.tsgrows the cases that would have caught this: two guest sockets carrying the same cookie hold one seat; two guest sockets with different cookies hold two; a socket with no cookie at all holds none and is a spectator; a signed-in socket's key is the forge id regardless of the cookie it also carries.Files
src/socket.ts,src/socket.test.ts,README.md.Risks
playerKeyreturns nothing when there is no session, and that path is the whole point — resist minting a key to make a type simpler, because minting is what this issue removes. A guest id is derived from a cookie the app setHttpOnly; it must never be read from a query string, a header a client chooses or a message the socket sends. And the upgrade has no response to set a cookie on, so the first visit is a page load then a socket — one round trip, which is the record's own answer and is worth restating in the pull.Fits Vesper's brief, with Sentinel on the pull: this is an authorization bug on the door that admits people to the world.