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
8 changes: 1 addition & 7 deletions src/iceberg/json_serde.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1705,13 +1705,7 @@ Result<std::unique_ptr<TableMetadata>> TableMetadataFromJson(const nlohmann::jso
}

Result<nlohmann::json> FromJsonString(const std::string& json_string) {
auto json =
nlohmann::json::parse(json_string, /*cb=*/nullptr, /*allow_exceptions=*/false);
if (json.is_discarded()) [[unlikely]] {
// Report the size only, not the raw input.
return JsonParseError("Failed to parse JSON string of {} bytes", json_string.size());
}
return json;
return ParseJsonString(json_string);
}

Result<std::string> ToJsonString(const nlohmann::json& json) {
Expand Down
8 changes: 2 additions & 6 deletions src/iceberg/puffin/json_serde.cc
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,8 @@ Result<FileMetadata> FileMetadataFromJsonString(std::string_view json_string) {
if (json_string.empty()) {
return JsonParseError("Cannot parse empty JSON string");
}
try {
auto json = nlohmann::json::parse(json_string);
return FileMetadataFromJson(json);
} catch (const nlohmann::json::parse_error& e) {
return JsonParseError("Failed to parse JSON: {}", e.what());
}
ICEBERG_ASSIGN_OR_RAISE(auto json, ParseJsonString(json_string));
return FileMetadataFromJson(json);
}

} // namespace iceberg::puffin
10 changes: 10 additions & 0 deletions src/iceberg/test/puffin_json_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "iceberg/puffin/file_metadata.h"
#include "iceberg/puffin/json_serde_internal.h"
#include "iceberg/test/matchers.h"
#include "iceberg/util/json_util_internal.h"

namespace iceberg::puffin {

Expand Down Expand Up @@ -198,4 +199,13 @@ TEST(PuffinJsonTest, FileMetadataFromInvalidString) {
IsError(ErrorKind::kJsonParseError));
}

TEST(PuffinJsonTest, FileMetadataRejectsDeeplyNestedJson) {
const std::string json = R"({"blobs":[],"properties":)" +
std::string(kMaxJsonNestingDepth, '[') +
std::string(kMaxJsonNestingDepth, ']') + "}";
auto result = FileMetadataFromJsonString(json);
EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
EXPECT_THAT(result, HasErrorMessage("nesting depth"));
}

} // namespace iceberg::puffin
22 changes: 22 additions & 0 deletions src/iceberg/test/schema_json_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "iceberg/schema_field.h"
#include "iceberg/test/matchers.h"
#include "iceberg/type.h"
#include "iceberg/util/json_util_internal.h"

namespace iceberg {

Expand Down Expand Up @@ -379,4 +380,25 @@ TEST(SchemaJsonTest, IdentifierFieldIds) {
ASSERT_EQ(json_multi_identifiers_out, json_multi_identifiers);
}

TEST(JsonNestingDepthTest, EnforcesConfiguredLimit) {
EXPECT_THAT(ParseJsonString("[[[]]]", /*max_depth=*/3), IsOk());
EXPECT_THAT(ParseJsonString(R"({"a":[{"value":"[{}]"}]})", /*max_depth=*/3), IsOk());
// Reject excessive nesting before reading the invalid trailing input.
for (const auto* json : {"[[[[invalid", R"({"a":{"b":{"c":{invalid)"}) {
SCOPED_TRACE(json);
auto result = ParseJsonString(json, /*max_depth=*/3);
EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
EXPECT_THAT(result, HasErrorMessage("nesting depth exceeds the maximum of 3"));
}
}

TEST(JsonNestingDepthTest, StringEntryPointEnforcesDefaultLimit) {
const std::string at_limit =
std::string(kMaxJsonNestingDepth, '[') + std::string(kMaxJsonNestingDepth, ']');
EXPECT_THAT(FromJsonString(at_limit), IsOk());
auto result = FromJsonString("[" + at_limit + "]");
EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
EXPECT_THAT(result, HasErrorMessage("nesting depth"));
}

} // namespace iceberg
33 changes: 33 additions & 0 deletions src/iceberg/util/json_util_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
#pragma once

#include <concepts>
#include <cstdint>
#include <format>
#include <limits>
#include <optional>
#include <string_view>

#include <nlohmann/json.hpp>

Expand Down Expand Up @@ -57,6 +59,37 @@ inline void SetOptionalStringField(nlohmann::json& json, std::string_view key,
}
}

/// \brief Default object/array nesting limit for parsed JSON documents.
inline constexpr int32_t kMaxJsonNestingDepth = 200;

/// \brief Parse JSON and stop before constructing containers beyond max_depth.
inline Result<nlohmann::json> ParseJsonString(std::string_view json_string,
int32_t max_depth = kMaxJsonNestingDepth) {
struct NestingDepthExceeded {};
const auto check_depth = [max_depth](int depth, nlohmann::json::parse_event_t event,
nlohmann::json&) {
if ((event == nlohmann::json::parse_event_t::object_start ||
event == nlohmann::json::parse_event_t::array_start) &&
depth >= max_depth) {
// Returning false only discards a value; throw to stop parsing immediately.
throw NestingDepthExceeded{};
}
return true;
};

try {
auto json = nlohmann::json::parse(json_string, check_depth,
/*allow_exceptions=*/false);
if (json.is_discarded()) [[unlikely]] {
return JsonParseError("Failed to parse JSON string of {} bytes",
json_string.size());
}
return json;
} catch (const NestingDepthExceeded&) {
return JsonParseError("JSON nesting depth exceeds the maximum of {}", max_depth);
}
}

/// \brief Dump a JSON document to a string, ignoring encoding errors.
///
/// Not intended for error messages: parse errors describe the offending key
Expand Down
Loading