Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions runtime/src/json/json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<Document> FromBackend(const nlohmann::json& value) {
switch (value.type()) {
case nlohmann::json::value_t::null:
Expand Down Expand Up @@ -99,6 +140,9 @@ std::string Encode(const Document& doc) {
}

Outcome<Document> 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()) {
Expand Down
21 changes: 21 additions & 0 deletions runtime/tests/json/json_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Loading