Merge pull request #3971 from arreina/fix/param-json-nesting

fix(libkernel): limit param.json nesting depth
This commit is contained in:
Dean 2026-10-13 02:33:55 +13:00 committed by GitHub
commit f4b395e28e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 31 additions and 9 deletions

View File

@ -160,9 +160,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;
@ -173,7 +176,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);
@ -184,14 +187,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);
@ -230,11 +234,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; }
@ -245,7 +249,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;

View File

@ -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");
@ -139,6 +156,7 @@ int main() {
}
CheckLanguages(path);
CheckUserDefinedParams(path);
CheckNesting(path);
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
result = 1;