Skip to content

Commit f46557b

Browse files
authored
fix: Add HTTP JSON parsing recursion depth limit (#8172)
1 parent 8d62bd8 commit f46557b

File tree

4 files changed

+85
-8
lines changed

4 files changed

+85
-8
lines changed

qa/L0_http/http_test.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/python
2-
# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
#
44
# Redistribution and use in source and binary forms, with or without
55
# modification, are permitted provided that the following conditions
@@ -273,6 +273,55 @@ def test_loading_large_invalid_model(self):
273273
except ValueError:
274274
self.fail("Response is not valid JSON")
275275

276+
def test_json_recursion_depth_limit(self):
277+
"""Test that server properly handles and rejects deeply nested JSON."""
278+
279+
def create_nested_json(depth, value):
280+
for _ in range(depth):
281+
value = f"[{value}]"
282+
return json.loads(value)
283+
284+
headers = {"Content-Type": "application/json"}
285+
test_matrix = [
286+
# (datatype, data, model, json_depth, should_succeed)
287+
("BYTES", '"hello"', "simple_identity", 120, False),
288+
("BYTES", '"hello"', "simple_identity", 50, True),
289+
("INT64", "123", "simple_identity_int64", 120, False),
290+
("INT64", "123", "simple_identity_int64", 50, True),
291+
]
292+
293+
for dtype, data, model, json_depth, should_succeed in test_matrix:
294+
with self.subTest(
295+
datatype=dtype, depth=json_depth, should_succeed=should_succeed
296+
):
297+
payload = {
298+
"inputs": [
299+
{
300+
"name": "INPUT0",
301+
"datatype": dtype,
302+
"shape": [1, 1],
303+
"data": create_nested_json(json_depth, data),
304+
}
305+
]
306+
}
307+
308+
response = requests.post(
309+
self._get_infer_url(model), headers=headers, json=payload
310+
)
311+
312+
if should_succeed:
313+
self.assertEqual(response.status_code, 200)
314+
else:
315+
self.assertNotEqual(response.status_code, 200)
316+
try:
317+
error_message = response.json().get("error", "")
318+
self.assertIn(
319+
"JSON nesting depth exceeds maximum allowed limit (100)",
320+
error_message,
321+
)
322+
except ValueError:
323+
self.fail("Response is not valid JSON")
324+
276325

277326
if __name__ == "__main__":
278327
unittest.main()

qa/L0_http/test.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,12 @@ cp -r ${MODELDIR}/onnx_zero_1_float32 ${MODELDIR}/onnx_zero_1_float32_queue && \
612612
echo " }" >> config.pbtxt && \
613613
echo "}" >> config.pbtxt)
614614

615+
cp -r ./models/simple_identity ${MODELDIR}
616+
cp -r ./models/simple_identity ${MODELDIR}/simple_identity_int64 && \
617+
(cd $MODELDIR/simple_identity_int64 && \
618+
sed -i "s/TYPE_STRING/TYPE_INT64/" config.pbtxt && \
619+
sed -i "s/simple_identity/simple_identity_int64/" config.pbtxt)
620+
615621
SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR}"
616622
SERVER_LOG="./inference_server_http_test.log"
617623
CLIENT_LOG="./http_test.log"
@@ -624,7 +630,7 @@ fi
624630

625631
TEST_RESULT_FILE='test_results.txt'
626632
PYTHON_TEST=http_test.py
627-
EXPECTED_NUM_TESTS=10
633+
EXPECTED_NUM_TESTS=11
628634
set +e
629635
python $PYTHON_TEST >$CLIENT_LOG 2>&1
630636
if [ $? -ne 0 ]; then

src/common.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+
// Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
//
33
// Redistribution and use in source and binary forms, with or without
44
// modification, are permitted provided that the following conditions
@@ -51,6 +51,9 @@ constexpr int MAX_GRPC_MESSAGE_SIZE = INT32_MAX;
5151
/// dimension can take on any size.
5252
constexpr int WILDCARD_DIM = -1;
5353

54+
// Maximum allowed depth for JSON parsing
55+
constexpr int32_t HTTP_MAX_JSON_NESTING_DEPTH = 100;
56+
5457
/// Request parameter keys that start with a "triton_" prefix for internal use
5558
const std::vector<std::string> TRITON_RESERVED_REQUEST_PARAMS{
5659
"triton_enable_empty_final_response"};

src/http_server.cc

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -437,16 +437,26 @@ AllocEVBuffer(const size_t byte_size, evbuffer** evb, void** base)
437437
// Recursively adds to byte_size from multi dimensional data input
438438
TRITONSERVER_Error*
439439
JsonBytesArrayByteSize(
440-
triton::common::TritonJson::Value& tensor_data, size_t* byte_size)
440+
triton::common::TritonJson::Value& tensor_data, size_t* byte_size,
441+
int current_depth = 0)
441442
{
443+
if (current_depth >= HTTP_MAX_JSON_NESTING_DEPTH) {
444+
return TRITONSERVER_ErrorNew(
445+
TRITONSERVER_ERROR_INVALID_ARG,
446+
("JSON nesting depth exceeds maximum allowed "
447+
"limit (" +
448+
std::to_string(HTTP_MAX_JSON_NESTING_DEPTH) + ")")
449+
.c_str());
450+
}
451+
442452
*byte_size = 0;
443453
// Recurse if not last dimension...
444454
if (tensor_data.IsArray()) {
445455
for (size_t i = 0; i < tensor_data.ArraySize(); i++) {
446456
triton::common::TritonJson::Value el;
447457
RETURN_IF_ERR(tensor_data.At(i, &el));
448458
size_t byte_size_;
449-
RETURN_IF_ERR(JsonBytesArrayByteSize(el, &byte_size_));
459+
RETURN_IF_ERR(JsonBytesArrayByteSize(el, &byte_size_, current_depth + 1));
450460
*byte_size += byte_size_;
451461
}
452462
} else {
@@ -466,20 +476,29 @@ TRITONSERVER_Error*
466476
ReadDataFromJsonHelper(
467477
char* base, const TRITONSERVER_DataType dtype,
468478
triton::common::TritonJson::Value& tensor_data, int* counter,
469-
int64_t expected_cnt)
479+
int64_t expected_cnt, int current_depth = 0)
470480
{
471481
// FIXME should move 'switch' statement outside the recursive function and
472482
// pass in a read data callback once data type is confirmed.
473483
// Currently 'switch' is performed on each element even through all elements
474484
// have the same data type.
475485

486+
if (current_depth >= HTTP_MAX_JSON_NESTING_DEPTH) {
487+
return TRITONSERVER_ErrorNew(
488+
TRITONSERVER_ERROR_INVALID_ARG,
489+
("JSON nesting depth exceeds maximum allowed "
490+
"limit (" +
491+
std::to_string(HTTP_MAX_JSON_NESTING_DEPTH) + ")")
492+
.c_str());
493+
}
494+
476495
// Recurse on array element if not last dimension...
477496
if (tensor_data.IsArray()) {
478497
for (size_t i = 0; i < tensor_data.ArraySize(); i++) {
479498
triton::common::TritonJson::Value el;
480499
RETURN_IF_ERR(tensor_data.At(i, &el));
481-
RETURN_IF_ERR(
482-
ReadDataFromJsonHelper(base, dtype, el, counter, expected_cnt));
500+
RETURN_IF_ERR(ReadDataFromJsonHelper(
501+
base, dtype, el, counter, expected_cnt, current_depth + 1));
483502
}
484503
} else {
485504
// Check if writing to 'serialized' is overrunning the expected byte_size

0 commit comments

Comments
 (0)