Security (S3):
- AuthHttpUtil/WebSocketHttpHandler: only honour the configured ws.ip.header
forwarded-IP header when the DIRECT peer is a trusted reverse proxy, instead
of trusting it unconditionally. Loopback is always trusted; extra proxies can
be allow-listed (exact IP or string prefix, comma-separated) via the new
`ws.ip.header.trusted` config key — default-deny so the header can't be
spoofed from the open internet to evade per-IP rate limiting and IP bans.
Also take only the first comma token when setting the game-session WS_IP.
Leak cleanup (C4):
- WiredVariableReferenceSupport.invalidateRoom(): drop a room's shared
wired-variable assignment caches; called from Room.dispose so the static
USER/ROOM_ASSIGNMENT_CACHE maps don't retain entries for the JVM lifetime.
- SessionResumeManager.parkHabbo: if the scheduler refuses the grace-expiry
task (future == null), disconnect immediately instead of parking an
un-reapable GhostSession that would pin the Habbo + room refs forever.
Note: ws.ip.header.trusted defaults to loopback-only; deployments whose proxy
is on another host must add its IP/prefix to that key or client IPs will
collapse to the proxy address.
Continuation of the concurrency hardening from the audit:
- InteractionWired/WiredHandler (E4): add an atomic per-box processing guard so
one trigger box is handled by a single thread at a time, making the cooldown
check-and-set effectively atomic; mark `cooldown` volatile. Prevents a packet
thread and the room cycle thread from double-firing the same wired stack
(double teleport/reward).
- RoomUnit (C1): the walk path is now a volatile ConcurrentLinkedDeque instead of
a plain LinkedList, so the room cycle popping steps can't corrupt it while a
walk packet rebuilds it via findPath/setPath.
- RoomItemManager (C2): iterate roomItems under its own monitor in getFloorItems/
getWallItems/getPostItNotes/getUserUniqueFurniCount/getItemsAt, matching the
existing put/remove sync sites — stops place/pickup from corrupting the
traversal into a silently-incomplete item set.
- pom.xml (S4): bump netty-all 4.1.115 -> 4.1.118.Final (CVE-2025-24970 SslHandler
pre-auth DoS, CVE-2025-25193).
From the full-codebase audit. Economy/security (Batch A):
- CatalogBuyItemEvent: clamp client `count` to 1..100 — the club-offer branch
accumulated cost in plain ints, so a huge count overflowed to a negative
total, bypassed the affordability checks and CREDITED the buyer (free
currency/subscription exploit).
- HousekeepingGiveCredits/GiveCurrency: bound `amount` to +/-1e9 to stop
overflow/negative-balance grants via the privileged path.
- RoomTrade: synchronize accept/confirm/offer/remove and add a `completed`
re-entry guard so two simultaneous confirms can't run tradeItems() twice
(item/credit duplication).
- HabboInfo: serialize credits + currencies read-modify-write and the
saveCurrencies snapshot on a dedicated lock (never held across DB I/O) —
fixes lost updates and Trove rehash-during-iteration corruption between the
credit-roller thread and purchase/trade handlers.
- AchievementManager/HabboStats: atomic incrementProgress() so concurrent
progress sources don't lose updates.
Resource/stability (Batch B):
- GameMessageRateLimit: release the wrapped ByteBuf on every drop path
(ClientMessage isn't ReferenceCounted, so the decoder's auto-release is a
no-op) — fixes a refcount leak on pre-auth/rate-limited packets.
- AuthRateLimiter: opportunistically evict window-expired STATE/PROBE_STATE
entries — previously grew unbounded, one entry per unique client IP.
- ForumThread/ForumThreadComment: close getGeneratedKeys() ResultSets via
try-with-resources, and create the first comment after the thread's
connection is released (was holding two pooled connections at once).
- DatabasePool: add socketTimeout/connectTimeout/tcpKeepAlive so a stalled
MariaDB can't pin a pooled connection (and its thread) indefinitely.
Concurrency visibility (Batch C, partial):
- Room: mark allowBotsWalk/allowPets/allowPetsEat volatile (read every cycle,
written from settings handlers on another thread).
- InteractionGift/OpenRecycleBoxEvent: add an atomic open-once guard so two
near-simultaneous OpenRecycleBox packets can't both schedule the async,
delayed OpenGift before the wrapper is removed (redundant double-process).
- ClientMessage.readString: treat the length prefix as unsigned (mask 0xFFFF)
and clamp to the buffered bytes, so a bogus/oversized length no longer
throws mid-read and desyncs the remaining fields of the packet.
Security
- HousekeepingSetUserRankEvent: add rank-ceiling guard (reject granting a
rank above the operator's own and modifying a higher-ranked target),
mirroring GiveRankCommand — closes a privilege-escalation path.
Trade integrity
- RoomTrade.clearAccepted now also resets confirmed; a stale confirmed=true
let a user strip their side and still complete once the partner re-confirms.
Concurrency
- RoomCycleManager: iterate the synchronized bot/pet maps under their own
monitor (lock order stays one-directional vs addBot/addPet — no deadlock).
- RoomSpecialTypes: synchronize nest/petDrink/petFood/petToy/petTree writers
on the same monitor their getters already use.
- HabboStats: synchronize achievement-progress map accessors.
- RebugKickBallAction: drop redundant direct mutation of the shared tile-cache
sets (updateTile invalidates them right after) — removes a data race.
Robustness
- Wired legacy parsers (HabboCount, NotHabboCount, MatchStatePosition,
MoveRotateFurni): guard length/format so one malformed row no longer aborts
the whole room's wired load.
- RoomLayout: fill malformed/short heightmap rows with INVALID tiles instead
of leaving nulls, and bounds-check door coordinates.
- FurnidataWatcher: defer (instead of drop) a throttled delta so furni-name
changes are never lost between broadcasts.
- GuildManager.getGuildMembers: fix LIMIT row-count (page size 14, not
offset+14) so member pages no longer overlap from page 1 on.
FriendsComposer only serialized a buddy's look when online, sending an
empty string for offline friends. The look is already loaded from the DB
for every friend in Messenger.loadFriends (SELECT users.look), so the
gate just discarded valid data: offline friends rendered with the
anonymous/standard avatar in the friend list and messenger, while their
profile (fetched separately) showed the real figure.
Always serialize row.getLook(). StaffChatBuddy keeps a non-null look
("ADM") so there is no NPE risk, and UpdateFriendComposer already sent
the look unconditionally, so this only aligns the initial friend list.
The names-server + watch + import config keys read by FurnitureTextProvider /
FurnidataWatcher / FurniEditorImportTextEvent were never seeded — a fresh
install logged 'Config key not found' for each and they were not DB-editable.
Seed portable defaults (items.furnidata.path empty → derives from
furni.editor.asset.base.path; booleans true/false; import URL = habbo.it).
utf8mb3 is deprecated (removed in MySQL 9.0) and can lose data on
emoji / 4-byte characters in audited furni names/descriptions. Use
utf8mb4/utf8mb4_unicode_ci (live table converted via ALTER).
FurniEditorImportTextEvent (incoming 10049, ACC_CATALOGFURNI): resolves
the classname, fetches the admin-configured furnidata URL via HttpClient
with a TTL cache (furni.editor.import.url / .cache.ms, default habbo.it),
finds name/description by classname and returns them via
FurniEditorImportTextResultComposer (outgoing 10049). URL is DB-configured
only (no client-supplied URL -> no SSRF); serves stale cache on failure.
Read sortField/sortDir from the search packet and ORDER BY a whitelisted
items_base column (id/sprite_id/item_name/public_name/type/interaction_type)
with a stable id tie-break, so sorting orders the whole result set instead
of just the page the client received. Column names come from a fixed
whitelist (never raw input) so the dynamic ORDER BY stays injection-safe.
On a successful furnidata name update (10046), after the JSON write +
10047 broadcast, also UPDATE items_base.public_name to the new
(sanitized) name and refresh the in-memory Item cache via loadItems()
so Item.getFullName() stays consistent without a restart. Guarded by
name != null (description-only edits never blank the column), runs only
on the success path, outside FurnidataLock, with a parameterized
statement.