Skip to content

security: bound unpack resize-before-read and harden producer HTTP - #4

Draft
igorls wants to merge 5 commits into
release/1.2.3-alpha2from
cursor/unpack-bounds-producer-http-82b1
Draft

igorls wants to merge 5 commits into
release/1.2.3-alpha2from
cursor/unpack-bounds-producer-http-82b1

Conversation

@igorls

@igorls igorls commented Sep 16, 2026

Copy link
Copy Markdown
Member

Follow-up to the v1.2.3-alpha2 remediations in #3. Completes the incomplete Issue-4 mitigation (F1) and hardens unauthenticated producer control-plane HTTP (F2). No consensus, ABI, wire, or RPC response-shape changes for valid traffic.

F1 — unpack allocation amplification (Medium)

fc::bounded_datastream stops cross-frame P2P reads, but fc::raw::unpack for std::vector / deque / flat_* still did resize/reserve(claimed_size) before reading elements. A ~25-byte notice_message can claim ids.size() == MAX_NUM_ARRAY_ELEMENTS (1<<20) and force ~32MiB vector<sha256> before the short-read fails. The same path is reachable from SHiP session unpack.

Fix: before resize/reserve, assert that count * pack_size(T{}) fits in s.remaining() when the stream reports a trustworthy remaining count (in-memory datastream and bounded_datastream). Default-constructed T is the minimum serialized size for the types on these paths (empty string/vector = 1-byte size prefix; sha256 = 32). Empty structs that pack to 0 skip the check so valid zero-payload vectors are not rejected.

This is fail-closed for malicious claims only. Valid payloads unpack unchanged.

Tests:

  • libraries/libfc/test/io/test_unpack_bounds.cpp — short frames with huge claimed sizes throw fc::assert_exception without a large RSS jump; valid vectors still round-trip.
  • plugins/net_plugin/tests/test_net_plugin.cpp — short bounded P2P notice_message with ids.size() == MAX_NUM_ARRAY_ELEMENTS is rejected.

F2 — producer_api_plugin unauthenticated HTTP (Medium, ops)

Destructive producer RPCs (pause/resume/snapshot/runtime options/feature schedule/whitelists) sit on the shared http_plugin listener with no auth. The previous warning only fired when the category was not loopback.

Hardening (safe for local tooling):

  • Default http-server-address (127.0.0.1) and UNIX sockets keep working with no new flags.
  • Non-loopback producer_rw / snapshot is refused at startup unless --http-expose-nonloopback-producer-api.
  • CORS origin + those APIs on a non-loopback listener is refused unless --http-allow-control-plane-cors.
  • Access-Control-Allow-Origin: * + Access-Control-Allow-Credentials: true is refused globally (invalid CORS, CSRF amplifier).

Residual risks (not invented as new vulns)

  • net_api_plugin net_rw (connect/disconnect) is the same warn-only unauthenticated pattern; not changed here.
  • File/cfile unpack still has no remaining(); the new check is skipped there (local/trusted inputs).
  • SHiP WebSocket still uses Beast’s default ~16MiB message cap; F1 stops amplification from short frames, not a full-size valid payload.
  • producer_ro remains unauthenticated (read-only).

Compatibility

No protocol-rule, ABI, or RPC/SHiP response-shape changes. Operators who intentionally bind producer APIs to a public HTTP address must add --http-expose-nonloopback-producer-api (and --http-allow-control-plane-cors if they also set a CORS origin).

Open in Web Open in Cursor 

cursoragent and others added 5 commits September 16, 2026 11:11
Reject claimed vector/deque/flat_* sizes that cannot fit in
stream remaining() before resize/reserve, so a short P2P/SHiP
frame advertising MAX_NUM_ARRAY_ELEMENTS cannot force a ~32MiB
allocation. Wire format is unchanged for valid payloads.

Refuse unauthenticated producer_rw/snapshot APIs on non-loopback
listeners unless --http-expose-nonloopback-producer-api is set,
refuse CORS on that combination without --http-allow-control-plane-cors,
and reject Access-Control-Allow-Origin=* with credentials.

Co-authored-by: Igor Lins e Silva <igorls@users.noreply.github.com>
id_list_modes is not FC_REFLECT_ENUM, so fc::raw writes sizeof(enum)
rather than int64_t. Packing 8 bytes misaligned the claimed ids size
and would not exercise the F1 remaining() check.

Co-authored-by: Igor Lins e Silva <igorls@users.noreply.github.com>
Clang rejects hash("a") because it instantiates hash<char[N]> and
fc::raw::pack is then overloaded for both T(&)[N] and const char*.
Use the std::string overload instead.

Co-authored-by: Igor Lins e Silva <igorls@users.noreply.github.com>
T dummy{} is copy-list-initialization and cannot call explicit
default constructors. Unpacking vector<book> failed to compile
because shared_cow_vector() is explicit. Use T dummy; instead.

Co-authored-by: Igor Lins e Silva <igorls@users.noreply.github.com>
Keep F1 short-read failures as fc::out_of_range_exception so existing
unpack tests and catch sites stay compatible. Read CORS origin and
credentials from the variables_map in http_plugin::plugin_initialize
because option notifiers may run after initialize.

Co-authored-by: Igor Lins e Silva <igorls@users.noreply.github.com>
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.

2 participants