Skip to content

fix(security): H1-H3 High findings from 2026-05-19 audit (supersedes #125)#167

Merged
Taure merged 2 commits into
mainfrom
security/highs-h1-h3
Jul 15, 2026
Merged

fix(security): H1-H3 High findings from 2026-05-19 audit (supersedes #125)#167
Taure merged 2 commits into
mainfrom
security/highs-h1-h3

Conversation

@Taure

@Taure Taure commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Re-lands the three unmerged High findings from the 2026-05-19 security audit (report kept internal, not in this public repo). Supersedes #125, which was 8 weeks stale.

Why not just merge #125

H4 (nova/cowlib bump) already landed on main independently — main is on cowlib 2.17.1, newer than #125's 2.16.1 target, so merging #125 as-is would have downgraded cowlib. H4 is dropped here; H1/H2/H3 were still genuinely open and are re-landed on current main.

Findings fixed

  • H1 — WS chat.join/chat.send now enforce asobi_chat_acl:authorized/2 (the shared dm/world/group predicate the HTTP history path already used). Previously WS join only validated the channel-id shape, so any authenticated player could join dm:<a>:<b> and eavesdrop. ACL logic extracted from asobi_chat_controller into asobi_chat_acl, consumed by both paths.
  • H2 — new asobi_body_cap_plugin (first pre_request plugin) rejects bodies > 1 MiB and chunked-without-content-length before nova_request_plugin buffers them into the heap.
  • H3asobi_world_lobby:list_worlds_cached/0,1: a 500 ms cache over world.list so a WS flood can't fan out synchronous get_info/1 calls per running world. find_or_create stays uncached (TOCTOU serialization preserved).

Review-driven hardening (3-agent gate: architecture-guardian → beam-security-reviewer → erlang-code-reviewer)

The review caught defects in the fixes themselves, all addressed:

  • H3 (High) — the cache was keyed on the raw, attacker-controlled filter map. mode is unbounded → a client cycling modes forced a miss every request (cache = no-op vs an attacker) and grew the ETS table without bound (a new memory-DoS inside a DoS mitigation). Now keyed on the has_capacity boolean only (≤2 rows), mode applied in-memory; mode also bounded to 64 B in the HTTP controller to match the WS path.
  • H2 (Medium)reject/4 returned {break,…}, which nova maps to a 2-tuple {ok,Req} that cowboy's stream handler has no clause for → case_clause crash of the request process on every 411/413 (log/CPU amplification). Now returns {stop,…}.
  • H3 (Low, defence-in-depth) — cache table was public (any in-node process could poison it). Now protected, written only by asobi_world_lobby_server via a cache_worlds/3 cast; callers still read directly.

Known / deferred

  • Low — with require_content_length => true, an HTTP/2 client sending a POST without content-length gets 411. First-party SDKs send content-length; flagged as a conscious config choice.
  • The audit's remaining Medium/Low findings are tracked internally, not here.

Test plan

  • rebar3 fmt --check ✓ · xref ✓ · dialyzer
  • elp eqwalize ✓ (all changed modules NO ERRORS)
  • elp lint ✓ (no findings in changed files)
  • rebar3 eunit — 324 tests, 0 failures
  • rebar3 ct249 tests, 0 failures (incl. new asobi_chat_ws_SUITE covering the H1 WS wiring end-to-end: third-party dm join/send rejected, member join allowed)

Taure added 2 commits July 15, 2026 06:14
H1 - route WS chat.join/chat.send through asobi_chat_acl:authorized/2,
the shared DM/world/group predicate the HTTP history endpoint already
uses. Without it any authenticated player could join dm:<a>:<b> and
eavesdrop. ACL logic extracted from asobi_chat_controller into
asobi_chat_acl and consumed by both paths.

H2 - asobi_body_cap_plugin rejects HTTP bodies > 1 MiB and chunked
requests without content-length before nova_request_plugin buffers
them into the heap. Wired as the first pre_request plugin.

H3 - asobi_world_lobby:list_worlds_cached/0,1 caches world.list for
500ms via an ETS table owned by asobi_world_lobby_server, stopping a
WS flood from fanning out synchronous get_info calls. find_or_create
stays uncached.

H4 (nova/cowlib bump) already landed on main independently - main is
on cowlib 2.17.1, newer than this audit's 2.16.1 target, so the H4
rebar changes are intentionally dropped.
Review of the H1-H3 re-land (asobi-architecture-guardian,
beam-security-reviewer, erlang-code-reviewer) surfaced two defects in the
fixes themselves plus test gaps:

H3 (High) - the cache was keyed on the raw, attacker-controlled filter
map. `mode` is unbounded, so a client cycling distinct modes forced a
miss on every request (defeating the cache) and grew the ETS table
without bound - a new memory-DoS inside a DoS mitigation. Now keyed on
the has_capacity boolean only (<=2 rows); mode is applied in-memory on
the cached list. Also bound `mode` to 64 bytes in the HTTP controller to
match the WS path.

H3 (Low, defence-in-depth) - cache table was `public`; any in-node
process could poison the shared world list. Now `protected` and written
only by asobi_world_lobby_server via a new cache_worlds/3 cast; callers
still read directly.

H2 (Medium) - reject/4 returned {break,...}, which nova maps to a
2-tuple {ok,Req} that cowboy's stream handler has no clause for ->
case_clause crash of the request process on every 411/413 (log/CPU
amplification). Now returns {stop,...}, which cowboy handles cleanly.

Tests: cache tests rewritten for the bounded-key scheme incl. a
distinct-modes-do-not-grow-table assertion; body-cap tests assert {stop};
new asobi_chat_ws_SUITE covers the H1 WS wiring end-to-end (third-party
dm join/send rejected, member join allowed).

Fixed a Unicode 'x' in a doc comment (ASCII only).
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