use log::warn;
use serde_json::Value;
pub const MAX_JSON_DEPTH: usize = 32;
pub fn validate_json_depth(json_str: &str, max_depth: usize) -> Result<(), String> {
let value: Value = serde_json::from_str(json_str).map_err(|e| {
warn!("JSON parse error: {}", e);
"Invalid JSON format".to_string()
})?;
let depth = calculate_depth(&value);
if depth > max_depth {
warn!(
"JSON depth validation failed: depth {} exceeds maximum {}",
depth, max_depth
);
return Err("JSON structure too deeply nested".to_string());
}
Ok(())
}
fn calculate_depth(value: &Value) -> usize {
calculate_depth_limited(value, 0)
}
fn calculate_depth_limited(value: &Value, current_depth: usize) -> usize {
if current_depth > MAX_JSON_DEPTH {
return current_depth;
}
match value {
Value::Array(arr) => {
if arr.is_empty() {
1
} else {
let max_child = arr
.iter()
.map(|v| calculate_depth_limited(v, current_depth.saturating_add(1)))
.max()
.unwrap_or(0);
1_usize.saturating_add(max_child)
}
}
Value::Object(obj) => {
if obj.is_empty() {
1
} else {
let max_child = obj
.values()
.map(|v| calculate_depth_limited(v, current_depth.saturating_add(1)))
.max()
.unwrap_or(0);
1_usize.saturating_add(max_child)
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => 0,
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_calculate_depth_scalar() {
let value = serde_json::json!("test");
assert_eq!(calculate_depth(&value), 0);
let value = serde_json::json!(42);
assert_eq!(calculate_depth(&value), 0);
let value = serde_json::json!(true);
assert_eq!(calculate_depth(&value), 0);
let value = serde_json::json!(null);
assert_eq!(calculate_depth(&value), 0);
}
#[test]
fn test_calculate_depth_simple_object() {
let value = serde_json::json!({"key": "value"});
assert_eq!(calculate_depth(&value), 1);
}
#[test]
fn test_calculate_depth_simple_array() {
let value = serde_json::json!([1, 2, 3]);
assert_eq!(calculate_depth(&value), 1);
}
#[test]
fn test_calculate_depth_nested_object() {
let value = serde_json::json!({
"user": {
"profile": {
"settings": {
"theme": "dark"
}
}
}
});
assert_eq!(calculate_depth(&value), 4);
}
#[test]
fn test_calculate_depth_nested_array() {
let value = serde_json::json!([[[1, 2], [3, 4]]]);
assert_eq!(calculate_depth(&value), 3);
}
#[test]
fn test_calculate_depth_mixed() {
let value = serde_json::json!({
"data": [
{"nested": [1, 2, 3]},
{"nested": [4, 5, 6]}
]
});
assert_eq!(calculate_depth(&value), 4);
}
#[test]
fn test_calculate_depth_empty_structures() {
let value = serde_json::json!({});
assert_eq!(calculate_depth(&value), 1);
let value = serde_json::json!([]);
assert_eq!(calculate_depth(&value), 1);
}
#[test]
fn test_validate_json_depth_valid() {
let json = r#"{"user": {"profile": {"name": "test"}}}"#;
assert!(validate_json_depth(json, MAX_JSON_DEPTH).is_ok());
}
#[test]
fn test_validate_json_depth_at_limit() {
let mut json = String::from("{");
for i in 0..MAX_JSON_DEPTH - 1 {
json.push_str(&format!("\"level{}\":{{", i));
}
json.push_str("\"value\":42");
json.push_str(&"}".repeat(MAX_JSON_DEPTH));
assert!(validate_json_depth(&json, MAX_JSON_DEPTH).is_ok());
}
#[test]
fn test_validate_json_depth_exceeds_limit() {
let mut json = String::from("{");
for i in 0..MAX_JSON_DEPTH + 5 {
json.push_str(&format!("\"level{}\":{{", i));
}
json.push_str("\"value\":42");
json.push_str(&"}".repeat(MAX_JSON_DEPTH + 6));
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
assert!(result.is_err());
assert!(
result
.expect_err("should fail on deeply nested json")
.contains("too deeply nested")
);
}
#[test]
fn test_validate_json_depth_invalid_json() {
let json = r#"{"invalid": json}"#;
let result = validate_json_depth(json, MAX_JSON_DEPTH);
assert!(result.is_err());
assert!(
result
.expect_err("should fail on invalid json")
.contains("Invalid JSON format")
);
}
#[test]
fn test_validate_json_depth_deeply_nested_array() {
let mut json = String::new();
for _ in 0..50 {
json.push('[');
}
json.push_str("42");
for _ in 0..50 {
json.push(']');
}
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
assert!(result.is_err());
assert!(
result
.expect_err("should fail on deeply nested json")
.contains("too deeply nested")
);
}
#[test]
fn test_validate_json_depth_custom_limit() {
let json = r#"{"a": {"b": {"c": {"d": "value"}}}}"#;
assert!(validate_json_depth(json, 5).is_ok());
let result = validate_json_depth(json, 3);
assert!(result.is_err());
}
#[test]
fn test_realistic_api_response() {
let json = r#"{
"_embedded": {
"applications": [
{
"id": 123,
"profile": {
"name": "TestApp",
"settings": {
"scan": {
"enabled": true
}
}
}
}
]
}
}"#;
assert!(validate_json_depth(json, MAX_JSON_DEPTH).is_ok());
}
#[test]
fn test_dos_payload_detection() {
let depth = 100;
let mut json = String::new();
for i in 0..depth {
json.push_str(&format!("{{\"level_{}\":", i));
}
json.push_str("null");
for _ in 0..depth {
json.push('}');
}
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
assert!(result.is_err());
assert!(
result
.expect_err("should fail on deeply nested json")
.contains("too deeply nested")
);
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod proptest_security {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn proptest_valid_json_within_limits_succeeds(
depth in 1usize..=MAX_JSON_DEPTH,
) {
let mut json = String::new();
for i in 0..depth {
json.push_str(&format!("{{\"level{}\":", i));
}
json.push_str("\"value\"");
json.push_str(&"}".repeat(depth));
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok(), "Valid JSON at depth {} should succeed", depth);
}
#[test]
fn proptest_deeply_nested_json_rejected(
excess_depth in 1usize..=50,
) {
let depth = MAX_JSON_DEPTH.saturating_add(excess_depth);
let mut json = String::new();
for i in 0..depth {
json.push_str(&format!("{{\"level{}\":", i));
}
json.push_str("null");
json.push_str(&"}".repeat(depth));
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_err(), "JSON at depth {} should be rejected", depth);
if let Err(msg) = result {
prop_assert!(
msg.contains("too deeply nested") || msg == "Invalid JSON format",
"Error message should indicate rejection: {}", msg
);
}
}
#[test]
fn proptest_invalid_json_returns_error(
garbage in ".*{0,200}",
) {
let result = validate_json_depth(&garbage, MAX_JSON_DEPTH);
match result {
Ok(_) => {
prop_assert!(serde_json::from_str::<Value>(&garbage).is_ok());
},
Err(msg) => {
prop_assert!(
msg == "Invalid JSON format" || msg.contains("too deeply nested"),
"Error message should be sanitized"
);
}
}
}
#[test]
fn proptest_empty_and_whitespace_json(
whitespace in "\\s{0,100}",
) {
let result = validate_json_depth(&whitespace, MAX_JSON_DEPTH);
match result {
Ok(_) => {
prop_assert!(serde_json::from_str::<Value>(&whitespace).is_ok());
},
Err(msg) => {
prop_assert_eq!(msg, "Invalid JSON format");
}
}
}
#[test]
fn proptest_custom_depth_limit_enforced(
max_depth in 5usize..=MAX_JSON_DEPTH,
test_depth in 1usize..=MAX_JSON_DEPTH,
) {
let mut json = String::new();
for i in 0..test_depth {
json.push_str(&format!("{{\"d{}\":", i));
}
json.push('0');
json.push_str(&"}".repeat(test_depth));
let result = validate_json_depth(&json, max_depth);
if test_depth <= max_depth {
prop_assert!(result.is_ok(),
"JSON depth {} should pass with limit {}", test_depth, max_depth);
} else {
prop_assert!(result.is_err(),
"JSON depth {} should fail with limit {}", test_depth, max_depth);
}
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn proptest_special_characters_in_strings(
special_chars in r#"[<>'"&\x00-\x1f\x7f\\]{0,100}"#,
) {
let json = serde_json::json!({
"payload": special_chars,
"nested": {
"value": special_chars
}
}).to_string();
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok());
let parsed: Value = serde_json::from_str(&json)
.expect("serde_json should handle its own output");
prop_assert_eq!(parsed.get("payload").and_then(|v| v.as_str()), Some(special_chars.as_str()));
}
#[test]
fn proptest_control_characters_safe(
control_char in prop::sample::select(vec![
'\0', '\t', '\n', '\r', '\x01', '\x02', '\x08', '\x0c', '\x1f', '\x7f'
]),
) {
let payload = format!("test{}value", control_char);
let json = serde_json::json!({
"data": payload
}).to_string();
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok());
}
#[test]
fn proptest_large_strings_safe(
length in 0usize..=10000,
) {
let large_string = "A".repeat(length);
let json = serde_json::json!({
"large_field": large_string
}).to_string();
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok());
let parsed: Value = serde_json::from_str(&json).expect("JSON parsing should succeed");
prop_assert_eq!(calculate_depth(&parsed), 1);
}
#[test]
fn proptest_unicode_handling(
unicode_str in "[\\p{L}\\p{N}\\p{S}\\p{M}]{0,200}",
) {
let json = serde_json::json!({
"unicode": unicode_str,
"nested": {
"more_unicode": unicode_str
}
}).to_string();
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok());
let parsed: Value = serde_json::from_str(&json).expect("JSON parsing should succeed");
prop_assert_eq!(parsed.get("unicode").and_then(|v| v.as_str()), Some(unicode_str.as_str()));
}
#[test]
fn proptest_path_traversal_sequences_safe(
traversal in prop::sample::select(vec![
"../", "..\\", "../../", "../../../etc/passwd",
"....//", "..\\..\\", "/etc/passwd", "C:\\Windows\\System32",
"%2e%2e%2f", "%2e%2e/", "..%2f", "..%5c"
]),
) {
let json = serde_json::json!({
"filename": traversal,
"path": traversal
}).to_string();
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok());
let parsed: Value = serde_json::from_str(&json).expect("JSON parsing should succeed");
prop_assert_eq!(parsed.get("filename").and_then(|v| v.as_str()), Some(traversal));
}
#[test]
fn proptest_null_byte_injection_safe(
prefix in "[a-zA-Z0-9]{0,50}",
suffix in "[a-zA-Z0-9]{0,50}",
) {
let payload = format!("{}\0{}", prefix, suffix);
let json = serde_json::json!({
"payload": payload
}).to_string();
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok());
let parsed: Value = serde_json::from_str(&json).expect("JSON parsing should succeed");
if let Some(s) = parsed.get("payload").and_then(|v| v.as_str()) {
let expected_without_null = format!("{}{}", prefix, suffix);
prop_assert!(s.contains('\0') || s == expected_without_null);
}
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn proptest_large_arrays_safe(
size in 0usize..=1000,
) {
let array: Vec<i32> = (0..size).map(|i| i32::try_from(i).unwrap_or(0)).collect();
let json = serde_json::json!({
"large_array": array
}).to_string();
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok());
let parsed: Value = serde_json::from_str(&json).expect("JSON parsing should succeed");
prop_assert_eq!(calculate_depth(&parsed), 2);
}
#[test]
fn proptest_large_objects_safe(
key_count in 0usize..=500,
) {
let mut obj = serde_json::Map::new();
for i in 0..key_count {
obj.insert(format!("key_{}", i), Value::from(i));
}
let json = Value::Object(obj).to_string();
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok());
let parsed: Value = serde_json::from_str(&json).expect("JSON parsing should succeed");
prop_assert_eq!(calculate_depth(&parsed), 1);
}
#[test]
fn proptest_empty_structures_depth(
nest_level in 0usize..=10,
) {
let mut json = String::new();
for _ in 0..nest_level {
json.push_str("{\"empty\":");
}
json.push_str("{}");
json.push_str(&"}".repeat(nest_level));
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok());
let parsed: Value = serde_json::from_str(&json).expect("JSON parsing should succeed");
let depth = calculate_depth(&parsed);
prop_assert_eq!(depth, nest_level.saturating_add(1));
}
#[test]
fn proptest_mixed_nesting_depth_calculation(
depth in 1usize..=10,
) {
let mut json = String::new();
for _ in 0..depth {
json.push_str("[{\"x\":");
}
json.push_str("null");
json.push_str(&"}]".repeat(depth));
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok(), "JSON construction should be valid");
let parsed: Value = serde_json::from_str(&json)
.expect("JSON should parse correctly");
let calculated_depth = calculate_depth(&parsed);
prop_assert!(calculated_depth >= depth,
"Calculated depth {} should be >= nesting levels {}",
calculated_depth, depth);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn proptest_depth_calculation_no_overflow(
depth in 0usize..=200,
) {
let mut json = String::new();
for i in 0..depth {
json.push_str(&format!("{{\"{}\":", i));
}
json.push_str("42");
json.push_str(&"}".repeat(depth));
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
if depth <= MAX_JSON_DEPTH {
prop_assert!(result.is_ok());
} else {
prop_assert!(result.is_err());
}
}
#[test]
fn proptest_extreme_numeric_values(
value in prop::num::i64::ANY,
) {
let json = serde_json::json!({
"number": value,
"nested": {
"another_number": value
}
}).to_string();
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_ok());
let parsed: Value = serde_json::from_str(&json).expect("JSON parsing should succeed");
prop_assert_eq!(calculate_depth(&parsed), 2);
}
#[test]
fn proptest_scalar_values_depth_zero(
bool_val in any::<bool>(),
) {
let null_value = Value::Null;
let bool_value = Value::Bool(bool_val);
let num_value = Value::from(42);
let str_value = Value::from("test");
prop_assert_eq!(calculate_depth(&null_value), 0);
prop_assert_eq!(calculate_depth(&bool_value), 0);
prop_assert_eq!(calculate_depth(&num_value), 0);
prop_assert_eq!(calculate_depth(&str_value), 0);
}
#[test]
fn proptest_recursion_bounded(
depth in (MAX_JSON_DEPTH + 1)..=60,
) {
let mut json = String::new();
for i in 0..depth {
json.push_str(&format!("[{{\"{}\":", i));
}
json.push('0');
json.push_str(&"}]".repeat(depth));
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
prop_assert!(result.is_err());
if let Ok(parsed) = serde_json::from_str::<Value>(&json) {
let calculated = calculate_depth(&parsed);
prop_assert!(calculated > MAX_JSON_DEPTH);
}
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 500 }, // Fewer cases due to complexity
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn proptest_dos_exponential_width(
width in 1usize..=20,
depth in 1usize..=4,
) {
fn create_wide_json(width: usize, depth: usize) -> String {
if depth == 0 {
return "42".to_string();
}
let mut json = String::from("[");
for i in 0..width {
if i > 0 {
json.push(',');
}
json.push_str(&create_wide_json(width, depth.saturating_sub(1)));
}
json.push(']');
json
}
let json = create_wide_json(width, depth);
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
if depth <= MAX_JSON_DEPTH {
prop_assert!(result.is_ok() || result.is_err()); } else {
prop_assert!(result.is_err());
}
}
#[test]
fn proptest_dos_varied_nesting_patterns(
depth in 30usize..=MAX_JSON_DEPTH + 20,
pattern in prop::sample::select(vec!["array", "object", "mixed"]),
) {
let json = match pattern {
"array" => {
let mut s = String::new();
for _ in 0..depth {
s.push('[');
}
s.push_str("null");
s.push_str(&"]".repeat(depth));
s
},
"object" => {
let mut s = String::new();
for i in 0..depth {
s.push_str(&format!("{{\"k{}\":", i));
}
s.push_str("null");
s.push_str(&"}".repeat(depth));
s
},
_ => { let mut s = String::new();
for i in 0..depth {
if i % 2 == 0 {
s.push('[');
} else {
s.push_str("{\"x\":");
}
}
s.push_str("null");
for i in (0..depth).rev() {
if i % 2 == 0 {
s.push(']');
} else {
s.push('}');
}
}
s
}
};
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
if depth <= MAX_JSON_DEPTH {
prop_assert!(result.is_ok());
} else {
prop_assert!(result.is_err());
}
}
#[test]
fn proptest_malformed_json_graceful_failure(
open_brackets in 0usize..=50,
close_brackets in 0usize..=50,
) {
let mut json = String::new();
json.push_str(&"[".repeat(open_brackets));
json.push_str("null");
json.push_str(&"]".repeat(close_brackets));
let result = validate_json_depth(&json, MAX_JSON_DEPTH);
match result {
Ok(_) => {
prop_assert_eq!(open_brackets, close_brackets);
},
Err(msg) => {
prop_assert!(
msg == "Invalid JSON format" || msg.contains("too deeply nested")
);
}
}
}
}
}