From e31b757c1ed3a317adc5e1f0c6c34a9da3dd6a2e Mon Sep 17 00:00:00 2001 From: Alberto Ruiz Date: Sun, 11 Oct 2026 21:13:01 +0200 Subject: [PATCH] fix(libkernel): limit param.json nesting depth parseJsonValue recursed without a limit, so a param.json with tens of thousands of nested arrays overflowed the stack. Reject nesting beyond 512 levels, the limit libSceJson2 uses. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01R5WJS4ZdQynAJesCZKH5hb --- .../AppMetadata/src/ParamJsonParser.cpp | 22 +++++++++++-------- core/libs/tests/ParamJsonParser.cpp | 18 +++++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 3645c476d..d7954ebf7 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -135,9 +135,12 @@ std::string parseJsonString(const std::string& text, std::size_t& position) { return result; } -JsonValue parseJsonValue(const std::string& text, std::size_t& position); +constexpr std::size_t MaxNestingDepth = 512; -JsonValue parseJsonObject(const std::string& text, std::size_t& position) { +JsonValue parseJsonValue(const std::string& text, std::size_t& position, std::size_t depth); + +JsonValue parseJsonObject(const std::string& text, std::size_t& position, std::size_t depth) { + if (depth >= MaxNestingDepth) throw std::runtime_error("param.json is nested too deeply"); expectChar(text, position, '{'); JsonValue value; value.type = JsonType::Object; @@ -148,7 +151,7 @@ JsonValue parseJsonObject(const std::string& text, std::size_t& position) { std::string key = parseJsonString(text, position); skipWhitespace(text, position); expectChar(text, position, ':'); - JsonValue member = parseJsonValue(text, position); + JsonValue member = parseJsonValue(text, position, depth + 1); value.objectValue.emplace_back(std::move(key), std::move(member)); skipWhitespace(text, position); const char next = peekChar(text, position); @@ -159,14 +162,15 @@ JsonValue parseJsonObject(const std::string& text, std::size_t& position) { return value; } -JsonValue parseJsonArray(const std::string& text, std::size_t& position) { +JsonValue parseJsonArray(const std::string& text, std::size_t& position, std::size_t depth) { + if (depth >= MaxNestingDepth) throw std::runtime_error("param.json is nested too deeply"); expectChar(text, position, '['); JsonValue value; value.type = JsonType::Array; skipWhitespace(text, position); if (peekChar(text, position) == ']') { ++position; return value; } while (true) { - JsonValue element = parseJsonValue(text, position); + JsonValue element = parseJsonValue(text, position, depth + 1); value.arrayValue.push_back(std::move(element)); skipWhitespace(text, position); const char next = peekChar(text, position); @@ -205,11 +209,11 @@ JsonValue parseJsonNumber(const std::string& text, std::size_t& position) { return value; } -JsonValue parseJsonValue(const std::string& text, std::size_t& position) { +JsonValue parseJsonValue(const std::string& text, std::size_t& position, std::size_t depth) { skipWhitespace(text, position); const char c = peekChar(text, position); - if (c == '{') return parseJsonObject(text, position); - if (c == '[') return parseJsonArray(text, position); + if (c == '{') return parseJsonObject(text, position, depth); + if (c == '[') return parseJsonArray(text, position, depth); if (c == '"') { JsonValue value; value.type = JsonType::String; value.stringValue = parseJsonString(text, position); return value; } if (c == 't') { expectLiteral(text, position, "true"); JsonValue value; value.type = JsonType::Boolean; value.boolValue = true; return value; } if (c == 'f') { expectLiteral(text, position, "false"); JsonValue value; value.type = JsonType::Boolean; value.boolValue = false; return value; } @@ -220,7 +224,7 @@ JsonValue parseJsonValue(const std::string& text, std::size_t& position) { JsonValue parseJsonDocument(const std::string& text) { std::size_t position = 0; - JsonValue root = parseJsonValue(text, position); + JsonValue root = parseJsonValue(text, position, 0); skipWhitespace(text, position); if (position != text.size()) throw std::runtime_error("unexpected trailing data in param.json"); return root; diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 653fb6745..22a108242 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -49,6 +49,23 @@ static void CheckUserDefinedParams(const std::filesystem::path& path) { } } +static void CheckNesting(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}},"extra":)"; + const auto nested = [&](std::size_t arrays) { + return prefix + std::string(arrays, '[') + std::string(arrays, ']') + "}"; + }; + Require(ParseText(path, nested(511)).title == "Example", "Nesting within the limit rejected"); + for (const std::size_t arrays : {std::size_t{512}, std::size_t{513}, std::size_t{100000}}) { + bool rejected = false; + try { + ParseText(path, nested(arrays)); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("nested too deeply") != std::string::npos; + } + Require(rejected, "Excessive nesting not rejected: " + std::to_string(arrays)); + } +} + static void CheckLanguages(const std::filesystem::path& path) { const auto nested = ParseText(path, "{\r\n \"localizedParameters\": {\r\n \"defaultLanguage\": \"en-GB\",\r\n" " \"en-GB\": { \"titleName\": \"British\" }\r\n },\r\n \"titleId\": \"PPSA00001\"\r\n}\r\n"); @@ -116,6 +133,7 @@ int main() { } CheckLanguages(path); CheckUserDefinedParams(path); + CheckNesting(path); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; result = 1;