Repository navigation
Conversation
nlohmann::json 3.1.2 parses recursively with no depth limit, and the parser recurses on every opening bracket, so a single line of [[[[... crashes the process. utility/json_depth.h pre-scans the input (O(n), no allocations) and rejects nesting deeper than 128 with a regular json::parse_error. Used for: wallet-api requests (HTTP and TCP), SBBS instant messages returned by read_messages, the stratum server of beam-node, replies of the swap bridges, the explorer's contract parser output. read_messages also returns a message that isn't acceptable JSON as a string instead of failing.
- explorer: include json_depth.h unconditionally, it was under BEAM_ASSET_SWAP_SUPPORT while ParseSafe uses it in every build - tests: 1 000 000 levels, 80 000 fit the 8 MB main thread and didn't crash without the fix
profinch
commented
Oct 6, 2026
profinch
left a comment
Author
There was a problem hiding this comment.
Review: one build break, fixed in 09025a6. Everything else checks out.
Fixed
explorer/adapter.cppincludedutility/json_depth.hinside#ifdef BEAM_ASSET_SWAP_SUPPORT, butParseSafecallsFindJsonDepthExcessin every build. With-DBEAM_ASSET_SWAP_SUPPORT=OFFthe file didn't compile (use of undeclared identifier 'FindJsonDepthExcess'). The include is now unconditional, and the file compiles with the option off.- The regression tests used 80 000 levels. That doesn't overflow the 8 MB main thread in
wallet_api_test, so without the fix the tests failed on a wrong error code instead of crashing. They now use 1 000 000 levels, which crashes the unfixed parser for sure.
Checked
FindJsonDepthExcessmatches the lexer on every prefix the parser accepts: strings, escapes and\\are handled, and stray closing brackets can't make the depth negative. So it bounds the real recursion depth. One pass, no allocations.- Errors are regular
parse_errors, socallGuardedmaps them to-32600as before. read_messages: returning a message that isn't valid JSON as a string is a reasonable change. Before, one bad message failed the whole call after the messages were already marked as read.- Tests on this branch (RelWithDebInfo, macOS arm64):
wallet_api_test,stratum_test,adapter_test,swap_board_test,wallet_testall pass. - End to end:
wallet-apiHTTP mode, 80 000 and 400 000 levels: rejected with-32600, the API keeps serving.wallet-apiTCP mode, one 65 000-[line: rejected with-32600, the API keeps serving.beam-nodestratum, 65 000[and 32 000 balanced levels: rejected, the node stays up andloginstill succeeds (#2147).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2144. Fixes #2147. An alternative to #2145, which covers only the wallet API. The comparison is below.
The bug is wider than the HTTP API
nlohmann::json 3.1.2 parses recursively with no depth limit, at ~190 bytes of stack per level in a release build. The parser recurses on every opening bracket before it ever sees a closing one, so
[[[[...without the closing brackets is enough. A 64 KB line holds ~65 000 levels, while the 8 MB main thread fits ~43 000. Before vs after, each with one unauthenticated request:wallet-api --use_http=1, 80 000 ×[+]-32600, keeps servingwallet-apiTCP (the default mode), 65 000 ×[in one line-32600beam-node --stratum_port, 65 000 ×[in one line, no auth neededread_messagesFix
utility/json_depth.h:FindJsonDepthExcess()is one O(n) pass with no allocations. It counts brackets outside of strings the way the lexer sees them, which bounds the depthjson::parsecan reach.ParseUntrustedJson()rejects nesting deeper thanMaxJsonDepth = 128with a regularjson::parse_error. Every existingcatchalready handles that: the API maps it toInvalidJsonRpc, the bridges to an invalid reply, stratum tomessage_corrupted. 128 is far beyond real documents and safe even on 512 KB secondary-thread stacks (measured: 2 790 levels fit).Used for everything that parses JSON from outside:
wallet/api/base/api_base.cpp: requests over HTTP, TCP, websockets and the apps API, plusfromError.wallet/api/v7_4,read_messages: instant messages are stored exactly as received from any sender. A message that isn't acceptable JSON (too deep, or not JSON at all) is now returned as a string. Before, a non-JSON message maderead_messagesfail, and sincereadIMsmarks new messages as read before the response is built, the whole batch was lost. Withall: truethe call failed every time.pow/stratum.cpp: miner connections tobeam-node.electrum.cpp,bitcoin_core_016.cpp,ethereum_bridge.cpp): replies from Electrum, bitcoind and Ethereum servers.explorer/adapter.cpp: the contract rich parser output.Local files (config, wallet DB import, wasm key keeper) are left as is.
Compared to #2145
Both use the same pre-scan idea and the same limit of 128. #2145 guards
ApiBaseonly, which covers HTTP and TCP requests. It leaves the remote crash through SBBS messages, thebeam-nodestratum port and the bridges open.Tests
wallet_api_test:paramsgetInvalidJsonRpc;read_messagesthrough the real v7.4 API with messages stored viastoreIM, like the wallet does on receipt: an 80 000-level message and a non-JSON one come back as strings, a normal one as an object. Against master'sv7_4_api_parse.cpp, the same test crashes (exit 139), and with only the non-JSON message it fails withInvalid JSON-RPC.stratum_test,bitcoin_rpc_test,electrum_test,swap_board_test,adapter_testpass.