From 764e3f56300b07a5d392d37307e749f900822c46 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 21:24:43 +0000 Subject: [PATCH] JSON: reject deeply nested input instead of overflowing the stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every generated simpleRestJson and jsonRpc2 server calls smithy::json::Decode() directly on the untrusted request body. A body of deeply nested arrays/objects (~20k levels) overflowed the stack in nlohmann's recursive-descent parser — and again in FromBackend — and crashed the process: an unauthenticated remote DoS. CBOR already guards this via DecodeValue's depth counter; JSON did not. Add an O(n), iterative pre-scan that rejects input past a fixed nesting depth (512, far beyond any legitimate document) before the recursive parser runs. Brackets inside strings don't nest, so the scan tracks string/escape state. Regression test drives 100k-deep arrays and objects through Decode and asserts a clean error, plus a bracket-heavy-but-flat document that must still parse. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019ytv3VMrURFYP2mdWhk3un --- runtime/src/json/json.cc | 44 +++++++++++++++++++++++++++++++++ runtime/tests/json/json_test.cc | 21 ++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/runtime/src/json/json.cc b/runtime/src/json/json.cc index c35b03cb..246a40a3 100644 --- a/runtime/src/json/json.cc +++ b/runtime/src/json/json.cc @@ -43,6 +43,47 @@ nlohmann::json ToBackend(const Document& doc) { return out; } +// Deeply nested JSON would overflow the stack in nlohmann's recursive-descent +// parser (and again in FromBackend) before any structural limit applies. This +// O(n), iterative pre-scan rejects input past a fixed nesting depth so a +// hostile body can't crash the process — the counterpart to CBOR's DecodeValue +// depth guard. Brackets inside strings don't nest, so track string state. +constexpr int kMaxNestingDepth = 512; + +bool ExceedsMaxDepth(std::string_view text) { + int depth = 0; + bool in_string = false; + bool escaped = false; + for (const char c : text) { + if (in_string) { + if (escaped) { + escaped = false; + } else if (c == '\\') { + escaped = true; + } else if (c == '"') { + in_string = false; + } + continue; + } + switch (c) { + case '"': + in_string = true; + break; + case '[': + case '{': + if (++depth > kMaxNestingDepth) return true; + break; + case ']': + case '}': + --depth; + break; + default: + break; + } + } + return false; +} + Outcome FromBackend(const nlohmann::json& value) { switch (value.type()) { case nlohmann::json::value_t::null: @@ -99,6 +140,9 @@ std::string Encode(const Document& doc) { } Outcome Decode(std::string_view text) { + if (ExceedsMaxDepth(text)) { + return Error::Serialization("json: nesting too deep"); + } const nlohmann::json parsed = nlohmann::json::parse(text, /*cb=*/nullptr, /*allow_exceptions=*/false); if (parsed.is_discarded()) { diff --git a/runtime/tests/json/json_test.cc b/runtime/tests/json/json_test.cc index df4408ed..fee123b2 100644 --- a/runtime/tests/json/json_test.cc +++ b/runtime/tests/json/json_test.cc @@ -87,6 +87,27 @@ TEST(JsonTest, EncodeReplacesInvalidUtf8InsteadOfThrowing) { EXPECT_EQ(Encode(Document(std::string("caf\xc3\xa9"))), "\"caf\xc3\xa9\""); } +// Security regression: every generated JSON server calls Decode() on the +// untrusted request body. Deeply nested input would overflow the stack in +// nlohmann's recursive-descent parser and crash the process; the depth guard +// must reject it as an ordinary error instead. ~100k levels is far past any +// legitimate document and reliably overflowed before the fix. +TEST(JsonTest, RejectsDeeplyNestedInputInsteadOfStackOverflow) { + for (const char open : {'[', '{'}) { + std::string bomb(100000, open); + // Well-formedness is irrelevant — the guard runs before the parser, so an + // unbalanced bomb is rejected for depth, not for being truncated. + const auto decoded = Decode(bomb); + EXPECT_FALSE(decoded.ok()); + } + // Brackets inside strings don't nest, so a flat document with bracket-heavy + // string content stays acceptable. + std::string wide = "["; + for (int i = 0; i < 1000; ++i) wide += R"("[[[[[[[[[[",)"; + wide += R"("end"])"; + EXPECT_TRUE(Decode(wide).ok()); +} + TEST(JsonTest, DecodesNestedLists) { const auto doc = Decode(R"([1,[2,"three"],null])"); ASSERT_TRUE(doc.ok());