Merge pull request #3784 from MrMha/fix/param-json-utf8

fix(libkernel): reject malformed UTF-8 in param JSON
This commit is contained in:
Adrià 2026-10-12 00:01:32 +02:00 committed by GitHub
commit 6566c9c8c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 56 additions and 1 deletions

View File

@ -95,6 +95,27 @@ std::uint32_t parseHex4(const std::string& text, std::size_t& position) {
return value;
}
void appendRawUtf8(const std::string& text, std::size_t& position, unsigned char first, std::string& result) {
unsigned count;
std::uint32_t codepoint;
std::uint32_t minimum;
if (first >= 0xC2 && first <= 0xDF) { count = 1; codepoint = first & 0x1F; minimum = 0x80; }
else if (first >= 0xE0 && first <= 0xEF) { count = 2; codepoint = first & 0x0F; minimum = 0x800; }
else if (first >= 0xF0 && first <= 0xF4) { count = 3; codepoint = first & 0x07; minimum = 0x10000; }
else throw std::runtime_error("invalid UTF-8 in param.json");
if (count > text.size() - position) throw std::runtime_error("truncated UTF-8 in param.json");
for (unsigned index = 0; index < count; ++index) {
const auto byte = static_cast<unsigned char>(text[position + index]);
if ((byte & 0xC0) != 0x80) throw std::runtime_error("invalid UTF-8 continuation in param.json");
codepoint = (codepoint << 6) | (byte & 0x3F);
}
if (codepoint < minimum || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF))
throw std::runtime_error("invalid UTF-8 codepoint in param.json");
result.push_back(static_cast<char>(first));
result.append(text, position, count);
position += count;
}
std::string parseJsonString(const std::string& text, std::size_t& position) {
expectChar(text, position, '"');
std::string result;
@ -103,7 +124,11 @@ std::string parseJsonString(const std::string& text, std::size_t& position) {
const char c = text[position++];
if (c == '"') break;
if (static_cast<unsigned char>(c) < 0x20) throw std::runtime_error("control character in param.json string");
if (c != '\\') { result.push_back(c); continue; }
if (c != '\\') {
if (static_cast<unsigned char>(c) < 0x80) result.push_back(c);
else appendRawUtf8(text, position, static_cast<unsigned char>(c), result);
continue;
}
if (position >= text.size()) throw std::runtime_error("unterminated escape in param.json");
const char escape = text[position++];
switch (escape) {

View File

@ -90,11 +90,34 @@ static void CheckNumberSyntax(const std::filesystem::path& path) {
}
}
static void CheckUtf8(const std::filesystem::path& path) {
const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":")";
const std::string suffix = R"("}}})";
for (const auto& title : {std::string("ASCII"), std::string("\xC2\x80"), std::string("\xE3\x82\xB2\xE3\x83\xBC\xE3\x83\xA0"),
std::string("\xF0\x9F\x8E\xAE"), std::string("\xF4\x8F\xBF\xBF")}) {
Require(ParseText(path, prefix + title + suffix).title == title, "Valid UTF-8 title changed");
}
Require(ParseText(path, prefix + R"(\uD83C\uDFAE)" + suffix).title == "\xF0\x9F\x8E\xAE", "Escaped title changed");
for (const auto& title : {std::string("\x80"), std::string("\xC0\xAF"), std::string("\xC1\xBF"), std::string("\xC2"),
std::string("\xE0\x80\xAF"), std::string("\xED\xA0\x80"), std::string("\xE3\x82"),
std::string("\xF0\x80\x80\xAF"), std::string("\xF4\x90\x80\x80"), std::string("\xF5\x80\x80\x80"),
std::string("\xFE"), std::string("\xFF"), std::string("\xC2x"), std::string("\xE3\x82x")}) {
bool rejected = false;
try { ParseText(path, prefix + title + suffix); } catch (const std::exception&) { rejected = true; }
Require(rejected, "Accepted malformed UTF-8 title");
}
bool rejected = false;
try { ParseText(path, std::string("{\"") + "\x80" + R"(":0,)" + prefix.substr(1) + "Valid" + suffix); }
catch (const std::exception&) { rejected = true; }
Require(rejected, "Accepted malformed UTF-8 object key");
}
int main() {
const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-json-" +
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".json");
int result = 0;
try {
CheckUtf8(path);
CheckNumberSyntax(path);
const std::pair<const char*, std::uint64_t> sizes[] = {
{"", 0}, {"0", 0}, {"-0", 0}, {"0.25", 0}, {"0e3", 0}, {"0E+3", 0}, {"-0e-3", 0}, {"4096", 4096},

View File

@ -202,6 +202,13 @@ if(BUILD_TESTING)
target_include_directories(buffer_bounds_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/io/include)
add_test(NAME buffer_bounds COMMAND buffer_bounds_tests)
add_test_executable(param_json_utf8_tests
${CMAKE_CURRENT_SOURCE_DIR}/../libs/tests/ParamJsonParser.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp)
target_include_directories(param_json_utf8_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs)
add_test(NAME param_json_utf8 COMMAND param_json_utf8_tests)
set_tests_properties(param_json_utf8 PROPERTIES TIMEOUT 20)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_test_executable(file_writer_tests
${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileWriterTests.cpp