common: bound JSON nesting depth when parsing
What changed, and why it matters
This commit fixes a stack-overflow risk in Core Lightning's JSON parser. Before the fix, an attacker could send a valid JSON-RPC message containing thousands of nested brackets or braces. The parser's own helper functions used recursion for each nesting level, so such input could crash the process by exhausting the C call stack. The patch adds an iterative depth check (maximum 256 levels) before any recursive walk, rejecting over-nested input safely.
Apply the patch and run the included unit test. Consider monitoring JSON-RPC endpoints for rejected over-nested payloads. No further immediate action is required; the fix is self-contained and does not change valid payload behavior for realistic nesting depths.
Security signals we found
Stack-overflow via deeply nested JSON
Recursive JSON traversal without depth bound
Denial-of-service vector in JSON-RPC input parsing
Iterative pre-validation to bound recursion
Changelog-Fixed explicitly labels JSON-RPC security fix
Evidence from the diff
The patch introduces bounded_datum_len() in common/json_parse_simple.c, an iterative validator that counts tokens in the first JSON datum and rejects anything nested deeper than JSON_MAX_NESTING (256). It is called inside json_parse_input() before json_next() or validate_jsmn_parse_output() run, both of which recurse once per nesting level. A test in common/test/run-json.c verifies that the limit is enforced and that pathologically deep (100,000-level) arrays and objects are rejected without crashing.
Changed components
common/json_parse_simple.cjson_parse_input()json_next()validate_jsmn_parse_output()Core Lightning JSON-RPC input parserInspect captured patch +90 / −2
### common/json_parse_simple.c
@@ -179,6 +179,47 @@ const jsmntok_t *json_next(const jsmntok_t *tok)
return t;
}
+/* We refuse JSON nested deeper than this. jsmn tokenizes iteratively, but
+ * json_next() and the validators below recurse once per nesting level, so an
+ * unbounded depth overflows the C stack. Real JSON-RPC and BOLT payloads nest
+ * only a handful of levels; this sits far above them and far below the stack
+ * limit. Enforced in json_parse_input(), so every token array handed to
+ * json_next() and friends elsewhere has already been bounded. */
+#define JSON_MAX_NESTING 256
+
+/* Iteratively count the tokens in the first datum of toks[], rejecting
+ * anything nested deeper than JSON_MAX_NESTING. On success sets *len to the
+ * token count (as json_next(toks) - toks would) and returns true; returns
+ * false without recursing on over-nested, attacker-controlled input. */
+static bool bounded_datum_len(const jsmntok_t *toks, size_t *len)
+{
+ /* remaining[d] = child datums still to visit at nesting level d;
+ * level 0 holds the single root datum. */
+ size_t remaining[JSON_MAX_NESTING + 1];
+ size_t depth = 0, i = 0;
+
+ remaining[0] = 1;
+ for (;;) {
+ /* Ascend out of every level we have finished. */
+ while (remaining[depth] == 0) {
+ if (depth == 0) {
+ *len = i;
+ return true;
+ }
+ depth--;
+ }
+ remaining[depth]--;
+
+ /* Descend into this token's children, if it has any. */
+ if (toks[i].size != 0) {
+ if (depth == JSON_MAX_NESTING)
+ return false;
+ remaining[++depth] = toks[i].size;
+ }
+ i++;
+ }
+}
+
const jsmntok_t *json_get_membern(const char *buffer,
const jsmntok_t tok[],
const char *label, size_t len)
@@ -492,8 +533,11 @@ bool json_parse_input(jsmn_parser *parser,
/* If we read a partial element at the end of the stream we'll get a
* errro, but due to the previous check we know we read at
* least one full element, so count tokens that are part of this root
- * element. */
- ret = json_next(*toks) - *toks;
+ * element. Bound the nesting depth here, before any recursive walk. */
+ size_t datumlen;
+ if (!bounded_datum_len(*toks, &datumlen))
+ return false;
+ ret = datumlen;
if (!validate_jsmn_parse_output(input, *toks, *toks + ret))
return false;
### common/test/run-json.c
@@ -226,6 +226,49 @@ static void test_json_bad_utf8(void)
assert(json_parse_simple(tmpctx, buf, strlen(buf)));
}
+static void test_json_deep_nesting(void)
+{
+ char *buf;
+ size_t d, i;
+
+ /* Arrays nested exactly at the limit still parse. */
+ d = JSON_MAX_NESTING;
+ buf = tal_arr(tmpctx, char, 2 * d + 2);
+ memset(buf, '[', d);
+ buf[d] = '0';
+ memset(buf + d + 1, ']', d);
+ buf[2 * d + 1] = '\0';
+ assert(json_parse_simple(tmpctx, buf, 2 * d + 1));
+
+ /* One level deeper is rejected, not crashed. */
+ d = JSON_MAX_NESTING + 1;
+ buf = tal_arr(tmpctx, char, 2 * d + 2);
+ memset(buf, '[', d);
+ buf[d] = '0';
+ memset(buf + d + 1, ']', d);
+ buf[2 * d + 1] = '\0';
+ assert(!json_parse_simple(tmpctx, buf, 2 * d + 1));
+
+ /* A pathologically deep array is rejected iteratively, without
+ * overflowing the stack. */
+ d = 100000;
+ buf = tal_arr(tmpctx, char, 2 * d + 2);
+ memset(buf, '[', d);
+ buf[d] = '0';
+ memset(buf + d + 1, ']', d);
+ buf[2 * d + 1] = '\0';
+ assert(!json_parse_simple(tmpctx, buf, 2 * d + 1));
+
+ /* Same for deeply nested objects. */
+ buf = tal_strdup(tmpctx, "");
+ for (i = 0; i < 100000; i++)
+ tal_append_fmt(&buf, "{\"a\":");
+ tal_append_fmt(&buf, "1");
+ for (i = 0; i < 100000; i++)
+ tal_append_fmt(&buf, "}");
+ assert(!json_parse_simple(tmpctx, buf, strlen(buf)));
+}
+
int main(int argc, char *argv[])
{
common_setup(argv[0]);
@@ -234,6 +277,7 @@ int main(int argc, char *argv[])
test_json_tok_bitcoin_amount();
test_json_tok_millionths();
test_json_bad_utf8();
+ test_json_deep_nesting();
common_shutdown();
}Why this scored 80/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.