Skip to content

Commit 5aee9eb

Browse files
added centralized param-pase limits
1 parent 1ab6a5c commit 5aee9eb

5 files changed

Lines changed: 452 additions & 52 deletions

File tree

src/loaders/json_loader.cpp

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#ifdef FLUXGRAPH_JSON_ENABLED
22

33
#include "fluxgraph/loaders/json_loader.hpp"
4+
#include "param_parse_limits.hpp"
45
#include <fstream>
56
#include <nlohmann/json.hpp>
67
#include <stdexcept>
@@ -11,18 +12,44 @@ namespace fluxgraph::loaders {
1112

1213
namespace {
1314

14-
ParamValue json_to_param_value(const json &j, const std::string &path) {
15+
ParamValue json_to_param_value(const json &j, const std::string &path,
16+
detail::ParamParseBudget &budget,
17+
size_t depth = 0) {
18+
budget.check_depth(depth, path);
19+
budget.consume_node(path);
20+
1521
if (j.is_number_float()) {
1622
return j.get<double>();
1723
} else if (j.is_number_integer()) {
1824
return j.get<int64_t>();
1925
} else if (j.is_boolean()) {
2026
return j.get<bool>();
2127
} else if (j.is_string()) {
22-
return j.get<std::string>();
28+
const std::string value = j.get<std::string>();
29+
detail::check_string_size(value.size(), path);
30+
return value;
31+
} else if (j.is_array()) {
32+
detail::check_array_size(j.size(), path);
33+
ParamArray arr;
34+
arr.reserve(j.size());
35+
for (size_t i = 0; i < j.size(); ++i) {
36+
arr.push_back(
37+
json_to_param_value(j[i], path + "/" + std::to_string(i), budget,
38+
depth + 1));
39+
}
40+
return arr;
41+
} else if (j.is_object()) {
42+
detail::check_object_size(j.size(), path);
43+
ParamObject obj;
44+
for (auto &[key, value] : j.items()) {
45+
detail::check_string_size(key.size(), path + "/<key>");
46+
obj.emplace(key, json_to_param_value(value, path + "/" + key, budget,
47+
depth + 1));
48+
}
49+
return obj;
2350
} else {
24-
throw std::runtime_error("JSON parse error at " + path +
25-
": Unsupported type for Variant");
51+
throw std::runtime_error("Parameter parse error at " + path +
52+
": Unsupported JSON type");
2653
}
2754
}
2855

@@ -37,13 +64,15 @@ Variant json_to_variant(const json &j, const std::string &path) {
3764
} else if (j.is_string()) {
3865
return j.get<std::string>();
3966
} else {
40-
throw std::runtime_error("JSON parse error at " + path +
41-
": Unsupported type for Variant");
67+
throw std::runtime_error(
68+
"JSON parse error at " + path +
69+
": Command args must be scalar (double/int64/bool/string)");
4270
}
4371
}
4472

4573
// Parse transform specification
46-
TransformSpec parse_transform(const json &j, const std::string &base_path) {
74+
TransformSpec parse_transform(const json &j, const std::string &base_path,
75+
detail::ParamParseBudget &budget) {
4776
TransformSpec spec;
4877

4978
std::string path = base_path + "/transform";
@@ -55,10 +84,14 @@ TransformSpec parse_transform(const json &j, const std::string &base_path) {
5584

5685
spec.type = j["type"].get<std::string>();
5786

58-
if (j.contains("params") && j["params"].is_object()) {
87+
if (j.contains("params")) {
88+
if (!j["params"].is_object()) {
89+
throw std::runtime_error("JSON parse error at " + path +
90+
"/params: Expected object");
91+
}
5992
for (auto &[key, value] : j["params"].items()) {
6093
std::string param_path = path + "/params/" + key;
61-
spec.params[key] = json_to_param_value(value, param_path);
94+
spec.params[key] = json_to_param_value(value, param_path, budget);
6295
}
6396
}
6497

@@ -85,7 +118,8 @@ SignalSpec parse_signal(const json &j, const std::string &base_path,
85118
}
86119

87120
// Parse edge specification
88-
EdgeSpec parse_edge(const json &j, const std::string &base_path, size_t index) {
121+
EdgeSpec parse_edge(const json &j, const std::string &base_path, size_t index,
122+
detail::ParamParseBudget &budget) {
89123
EdgeSpec spec;
90124

91125
std::string path = base_path + "/" + std::to_string(index);
@@ -102,17 +136,21 @@ EdgeSpec parse_edge(const json &j, const std::string &base_path, size_t index) {
102136
throw std::runtime_error("JSON parse error at " + path +
103137
": Missing required field 'transform'");
104138
}
139+
if (!j["transform"].is_object()) {
140+
throw std::runtime_error("JSON parse error at " + path +
141+
"/transform: Expected object");
142+
}
105143

106144
spec.source_path = j["source"].get<std::string>();
107145
spec.target_path = j["target"].get<std::string>();
108-
spec.transform = parse_transform(j["transform"], path);
146+
spec.transform = parse_transform(j["transform"], path, budget);
109147

110148
return spec;
111149
}
112150

113151
// Parse model specification
114152
ModelSpec parse_model(const json &j, const std::string &base_path,
115-
size_t index) {
153+
size_t index, detail::ParamParseBudget &budget) {
116154
ModelSpec spec;
117155

118156
std::string path = base_path + "/" + std::to_string(index);
@@ -129,10 +167,14 @@ ModelSpec parse_model(const json &j, const std::string &base_path,
129167
spec.id = j["id"].get<std::string>();
130168
spec.type = j["type"].get<std::string>();
131169

132-
if (j.contains("params") && j["params"].is_object()) {
170+
if (j.contains("params")) {
171+
if (!j["params"].is_object()) {
172+
throw std::runtime_error("JSON parse error at " + path +
173+
"/params: Expected object");
174+
}
133175
for (auto &[key, value] : j["params"].items()) {
134176
std::string param_path = path + "/params/" + key;
135-
spec.params[key] = json_to_param_value(value, param_path);
177+
spec.params[key] = json_to_param_value(value, param_path, budget);
136178
}
137179
}
138180

@@ -176,7 +218,11 @@ RuleSpec parse_rule(const json &j, const std::string &base_path, size_t index) {
176218
action.device = action_json["device"].get<std::string>();
177219
action.function = action_json["function"].get<std::string>();
178220

179-
if (action_json.contains("args") && action_json["args"].is_object()) {
221+
if (action_json.contains("args")) {
222+
if (!action_json["args"].is_object()) {
223+
throw std::runtime_error("JSON parse error at " + action_path +
224+
"/args: Expected object");
225+
}
180226
for (auto &[key, value] : action_json["args"].items()) {
181227
std::string arg_path = action_path + "/args/" + key;
182228
action.args[key] = json_to_variant(value, arg_path);
@@ -199,6 +245,7 @@ RuleSpec parse_rule(const json &j, const std::string &base_path, size_t index) {
199245
// Parse complete GraphSpec from JSON
200246
GraphSpec parse_json(const json &j) {
201247
GraphSpec spec;
248+
detail::ParamParseBudget param_budget;
202249

203250
// Parse signals (optional)
204251
if (j.contains("signals") && j["signals"].is_array()) {
@@ -212,15 +259,17 @@ GraphSpec parse_json(const json &j) {
212259
if (j.contains("models") && j["models"].is_array()) {
213260
size_t index = 0;
214261
for (auto &model_json : j["models"]) {
215-
spec.models.push_back(parse_model(model_json, "/models", index++));
262+
spec.models.push_back(
263+
parse_model(model_json, "/models", index++, param_budget));
216264
}
217265
}
218266

219267
// Parse edges (optional)
220268
if (j.contains("edges") && j["edges"].is_array()) {
221269
size_t index = 0;
222270
for (auto &edge_json : j["edges"]) {
223-
spec.edges.push_back(parse_edge(edge_json, "/edges", index++));
271+
spec.edges.push_back(
272+
parse_edge(edge_json, "/edges", index++, param_budget));
224273
}
225274
}
226275

src/loaders/param_parse_limits.hpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <stdexcept>
5+
#include <string>
6+
7+
namespace fluxgraph::loaders::detail {
8+
9+
struct ParamParseLimits {
10+
static constexpr size_t kMaxDepth = 32;
11+
static constexpr size_t kMaxNodes = 250000;
12+
static constexpr size_t kMaxObjectMembers = 4096;
13+
static constexpr size_t kMaxArrayElements = 65536;
14+
static constexpr size_t kMaxStringBytes = 1 << 20; // 1 MiB
15+
};
16+
17+
struct ParamParseBudget {
18+
size_t nodes = 0;
19+
20+
void consume_node(const std::string &path) {
21+
++nodes;
22+
if (nodes > ParamParseLimits::kMaxNodes) {
23+
throw std::runtime_error("Parameter parse error at " + path +
24+
": node count exceeds limit (" +
25+
std::to_string(ParamParseLimits::kMaxNodes) +
26+
")");
27+
}
28+
}
29+
30+
void check_depth(size_t depth, const std::string &path) const {
31+
if (depth > ParamParseLimits::kMaxDepth) {
32+
throw std::runtime_error("Parameter parse error at " + path +
33+
": nesting depth exceeds limit (" +
34+
std::to_string(ParamParseLimits::kMaxDepth) +
35+
")");
36+
}
37+
}
38+
};
39+
40+
inline void check_object_size(size_t size, const std::string &path) {
41+
if (size > ParamParseLimits::kMaxObjectMembers) {
42+
throw std::runtime_error("Parameter parse error at " + path +
43+
": object member count exceeds limit (" +
44+
std::to_string(ParamParseLimits::kMaxObjectMembers) +
45+
")");
46+
}
47+
}
48+
49+
inline void check_array_size(size_t size, const std::string &path) {
50+
if (size > ParamParseLimits::kMaxArrayElements) {
51+
throw std::runtime_error("Parameter parse error at " + path +
52+
": array length exceeds limit (" +
53+
std::to_string(ParamParseLimits::kMaxArrayElements) +
54+
")");
55+
}
56+
}
57+
58+
inline void check_string_size(size_t size, const std::string &path) {
59+
if (size > ParamParseLimits::kMaxStringBytes) {
60+
throw std::runtime_error("Parameter parse error at " + path +
61+
": string length exceeds limit (" +
62+
std::to_string(ParamParseLimits::kMaxStringBytes) +
63+
" bytes)");
64+
}
65+
}
66+
67+
} // namespace fluxgraph::loaders::detail

0 commit comments

Comments
 (0)