Skip to content

security: bound room creation and reclaim empty rooms (DoS)#34

Merged
krotname merged 1 commit into
mainfrom
security/fix-room-exhaustion
Jul 23, 2026
Merged

security: bound room creation and reclaim empty rooms (DoS)#34
krotname merged 1 commit into
mainfrom
security/fix-room-exhaustion

Conversation

@krotname

Copy link
Copy Markdown
Owner

Fixes the remaining denial-of-service issue found in the security audit. The unbounded pre-handshake frame read was already fixed upstream and is untouched here.

The bug. Room entries were never reclaimed: handleRoomLeave removed only the caller and removeSession removed the user from every member set but never deleted the room key, so roomMembers grew monotonically for the process lifetime. There was no rate limiting, so one client could spam unique ROOM_JOIN names to leak memory and force an N-way ROOM_ADDED broadcast per join. In the default no---accounts mode this is unauthenticated.

  • Reclamation — a non-general room is deleted when its last member leaves or disconnects, broadcasting the new ROOM_REMOVED frame. general is never reclaimed.
  • Configurable capmaxRooms, default 256, CLI --max-rooms. The previous hardcoded 1000 was bypassable via joinRoom's computeIfAbsent.
  • Rate limiting — per-connection token buckets on inbound frames and room creation; over-limit replies ERROR rather than disconnecting.
  • Locking — a dedicated roomsMonitor makes the cap exact; no I/O under the monitor and the two locks are acyclic.

./gradlew clean check → BUILD SUCCESSFUL, 105 tests, 0 failures; checkstyle, spotless, spotbugs and coverage all green.

⚠️ Protocol note: PROTOCOL_VERSION stays 1, but an old client build receiving ROOM_REMOVED will drop the connection. In a mixed-version deployment, upgrade clients before servers.

🤖 Generated with Claude Code

`ChatServer` created a room entry for every new `ROOM_JOIN` name and never
removed one: `handleRoomLeave` dropped only the caller and `removeSession`
dropped the user from every member set without deleting empty room keys. The
map therefore grew monotonically for the process lifetime, and with no rate
limiting a single client could spam unique room names to exhaust memory and
force an N-way `ROOM_ADDED` broadcast per join. In the default no-`--accounts`
mode this was unauthenticated.

- Reclaim a non-default room as soon as its last member leaves or disconnects,
  and announce it with the new `ROOM_REMOVED` frame plus a
  `ChatMessage.roomRemoved` factory. `general` is never reclaimed.
- Handle `ROOM_REMOVED` in the console client and the Swing controller/model so
  room lists do not go stale.
- Replace the hardcoded 1,000-room constant with `ChatServerConfig.maxRooms`
  (default 256, CLI `--max-rooms`); an over-cap `ROOM_JOIN` for a new room is
  answered with `ERROR` instead of creating it.
- Add per-connection token buckets (`RateLimitConfig`, `TokenBucket`) for
  inbound frames and for room creation; exhausted buckets reply `ERROR` rather
  than dropping the connection.

Membership, the cap check, and reclamation run under a dedicated rooms monitor
so the cap is exact and a room cannot be reclaimed between a joiner observing it
and joining it. That monitor performs no I/O and is never nested inside the
sessions monitor: `removeSession` now announces `USER_REMOVED`/`ROOM_REMOVED`
after releasing the sessions monitor, so the two locks have no cycle.

Tests cover reclamation on last leave and on disconnect, the broadcast, the
general-room exemption, the room cap, and both token buckets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.11111% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...a/dev/krotname/networkchat/network/ChatServer.java 86.36% 8 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5a35dd41a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

ROOM_JOIN,
ROOM_LEAVE,
ROOM_ADDED,
ROOM_REMOVED,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bump the protocol version for ROOM_REMOVED

When a mixed-version deployment has any older v1 client connected, reclaiming a room now broadcasts ROOM_REMOVED with the still-current protocol version 1. Those clients do not have this enum value, so ChatProtocol.decode fails before their version check can produce an explicit unsupported-version error, and the client drops the connection whenever another user empties a room. Please either increment the protocol version for this new frame or avoid sending it to protocol-v1 clients.

Useful? React with 👍 / 👎.

Comment on lines +329 to 333
synchronized (roomsMonitor) {
roomNames = List.copyOf(roomMembers.keySet());
}
for (String roomName : roomNames) {
connection.send(ChatMessage.roomAdded(roomName));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve removal ordering during initial room sync

When a client is connecting while the last member leaves a room, the new connection is already in sessions, so announceRoomRemoved can send ROOM_REMOVED to it after this snapshot is taken but before the loop sends the stale ROOM_ADDED. The client then applies remove-before-add and keeps a room that no longer exists until it tries to use it. Please order the initial room list with removal broadcasts, or revalidate/suppress stale room additions before sending them.

Useful? React with 👍 / 👎.

@krotname
krotname merged commit 751a370 into main Jul 23, 2026
9 checks passed
@krotname
krotname deleted the security/fix-room-exhaustion branch July 23, 2026 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant