veracode_platform/
json_validator.rs

1//! JSON validation utilities to prevent `DoS` attacks
2//!
3//! This module provides functions to validate JSON structure before deserialization,
4//! preventing Denial of Service attacks through deeply nested JSON structures.
5
6use serde_json::Value;
7
8/// Maximum allowed JSON nesting depth
9///
10/// This limit prevents `DoS` attacks via deeply nested `JSON` that can cause:
11/// - Stack overflow
12/// - Excessive memory consumption
13/// - CPU exhaustion during parsing
14///
15/// A depth of 32 is sufficient for legitimate API responses while protecting
16/// against malicious payloads. Most real-world APIs use <10 levels of nesting.
17pub const MAX_JSON_DEPTH: usize = 32;
18
19/// Validate JSON nesting depth to prevent `DoS` attacks
20///
21/// # Arguments
22///
23/// * `json_str` - The JSON string to validate
24/// * `max_depth` - Maximum allowed nesting depth (use `MAX_JSON_DEPTH` for default)
25///
26/// # Returns
27///
28/// * `Ok(())` if the JSON is valid and within depth limits
29/// * `Err(String)` with error message if validation fails
30///
31/// # Examples
32///
33/// ```
34/// use veracode_platform::json_validator::{validate_json_depth, MAX_JSON_DEPTH};
35///
36/// // Valid JSON within depth limit
37/// let json = r#"{"user": {"profile": {"settings": {"theme": "dark"}}}}"#;
38/// assert!(validate_json_depth(json, MAX_JSON_DEPTH).is_ok());
39///
40/// // Deeply nested JSON should be rejected
41/// let deep_json = (0..50).fold(String::from("{\"a\":"), |acc, _| acc + "{\"a\":")
42///     + &(0..50).map(|_| "}").collect::<String>();
43/// assert!(validate_json_depth(&deep_json, MAX_JSON_DEPTH).is_err());
44/// ```
45///
46/// # Security
47///
48/// This function protects against:
49/// - Stack overflow from recursive parsing
50/// - CPU exhaustion from excessive nesting
51/// - Memory exhaustion from deeply nested structures
52///
53/// # Errors
54///
55/// Returns an error if the JSON is invalid or exceeds the maximum nesting depth.
56pub fn validate_json_depth(json_str: &str, max_depth: usize) -> Result<(), String> {
57    // First, try to parse the JSON
58    let value: Value =
59        serde_json::from_str(json_str).map_err(|e| format!("Invalid JSON: {}", e))?;
60
61    // Calculate the actual nesting depth
62    let depth = calculate_depth(&value);
63
64    if depth > max_depth {
65        return Err(format!(
66            "JSON nesting depth {} exceeds maximum allowed depth of {}",
67            depth, max_depth
68        ));
69    }
70
71    Ok(())
72}
73
74/// Calculate the maximum nesting depth of a JSON value
75///
76/// # Arguments
77///
78/// * `value` - The JSON value to analyze
79///
80/// # Returns
81///
82/// The maximum nesting depth (0 for scalars, 1+ for nested structures)
83fn calculate_depth(value: &Value) -> usize {
84    match value {
85        Value::Array(arr) => {
86            if arr.is_empty() {
87                1
88            } else {
89                1_usize.saturating_add(arr.iter().map(calculate_depth).max().unwrap_or(0))
90            }
91        }
92        Value::Object(obj) => {
93            if obj.is_empty() {
94                1
95            } else {
96                1_usize.saturating_add(obj.values().map(calculate_depth).max().unwrap_or(0))
97            }
98        }
99        // Scalars have depth 0
100        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => 0,
101    }
102}
103
104#[cfg(test)]
105#[allow(clippy::expect_used)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_calculate_depth_scalar() {
111        let value = serde_json::json!("test");
112        assert_eq!(calculate_depth(&value), 0);
113
114        let value = serde_json::json!(42);
115        assert_eq!(calculate_depth(&value), 0);
116
117        let value = serde_json::json!(true);
118        assert_eq!(calculate_depth(&value), 0);
119
120        let value = serde_json::json!(null);
121        assert_eq!(calculate_depth(&value), 0);
122    }
123
124    #[test]
125    fn test_calculate_depth_simple_object() {
126        let value = serde_json::json!({"key": "value"});
127        assert_eq!(calculate_depth(&value), 1);
128    }
129
130    #[test]
131    fn test_calculate_depth_simple_array() {
132        let value = serde_json::json!([1, 2, 3]);
133        assert_eq!(calculate_depth(&value), 1);
134    }
135
136    #[test]
137    fn test_calculate_depth_nested_object() {
138        let value = serde_json::json!({
139            "user": {
140                "profile": {
141                    "settings": {
142                        "theme": "dark"
143                    }
144                }
145            }
146        });
147        assert_eq!(calculate_depth(&value), 4);
148    }
149
150    #[test]
151    fn test_calculate_depth_nested_array() {
152        let value = serde_json::json!([[[1, 2], [3, 4]]]);
153        assert_eq!(calculate_depth(&value), 3);
154    }
155
156    #[test]
157    fn test_calculate_depth_mixed() {
158        let value = serde_json::json!({
159            "data": [
160                {"nested": [1, 2, 3]},
161                {"nested": [4, 5, 6]}
162            ]
163        });
164        // Depth: root object (1) + array (1) + inner objects (1) + inner arrays (1) = 4
165        assert_eq!(calculate_depth(&value), 4);
166    }
167
168    #[test]
169    fn test_calculate_depth_empty_structures() {
170        let value = serde_json::json!({});
171        assert_eq!(calculate_depth(&value), 1);
172
173        let value = serde_json::json!([]);
174        assert_eq!(calculate_depth(&value), 1);
175    }
176
177    #[test]
178    fn test_validate_json_depth_valid() {
179        let json = r#"{"user": {"profile": {"name": "test"}}}"#;
180        assert!(validate_json_depth(json, MAX_JSON_DEPTH).is_ok());
181    }
182
183    #[test]
184    fn test_validate_json_depth_at_limit() {
185        // Create JSON at exactly MAX_JSON_DEPTH
186        let mut json = String::from("{");
187        for i in 0..MAX_JSON_DEPTH - 1 {
188            json.push_str(&format!("\"level{}\":{{", i));
189        }
190        json.push_str("\"value\":42");
191        json.push_str(&"}".repeat(MAX_JSON_DEPTH));
192
193        assert!(validate_json_depth(&json, MAX_JSON_DEPTH).is_ok());
194    }
195
196    #[test]
197    fn test_validate_json_depth_exceeds_limit() {
198        // Create JSON that exceeds MAX_JSON_DEPTH
199        let mut json = String::from("{");
200        for i in 0..MAX_JSON_DEPTH + 5 {
201            json.push_str(&format!("\"level{}\":{{", i));
202        }
203        json.push_str("\"value\":42");
204        json.push_str(&"}".repeat(MAX_JSON_DEPTH + 6));
205
206        let result = validate_json_depth(&json, MAX_JSON_DEPTH);
207        assert!(result.is_err());
208        assert!(
209            result
210                .expect_err("should fail on deeply nested json")
211                .contains("exceeds maximum allowed depth")
212        );
213    }
214
215    #[test]
216    fn test_validate_json_depth_invalid_json() {
217        let json = r#"{"invalid": json}"#;
218        let result = validate_json_depth(json, MAX_JSON_DEPTH);
219        assert!(result.is_err());
220        assert!(
221            result
222                .expect_err("should fail on invalid json")
223                .contains("Invalid JSON")
224        );
225    }
226
227    #[test]
228    fn test_validate_json_depth_deeply_nested_array() {
229        // Create deeply nested array
230        let mut json = String::new();
231        for _ in 0..50 {
232            json.push('[');
233        }
234        json.push_str("42");
235        for _ in 0..50 {
236            json.push(']');
237        }
238
239        let result = validate_json_depth(&json, MAX_JSON_DEPTH);
240        assert!(result.is_err());
241        assert!(
242            result
243                .expect_err("should fail on deeply nested json")
244                .contains("exceeds maximum allowed depth")
245        );
246    }
247
248    #[test]
249    fn test_validate_json_depth_custom_limit() {
250        let json = r#"{"a": {"b": {"c": {"d": "value"}}}}"#;
251
252        // Should pass with limit 5
253        assert!(validate_json_depth(json, 5).is_ok());
254
255        // Should fail with limit 3
256        let result = validate_json_depth(json, 3);
257        assert!(result.is_err());
258    }
259
260    #[test]
261    fn test_realistic_api_response() {
262        // Simulate a realistic API response with moderate nesting
263        let json = r#"{
264            "_embedded": {
265                "applications": [
266                    {
267                        "id": 123,
268                        "profile": {
269                            "name": "TestApp",
270                            "settings": {
271                                "scan": {
272                                    "enabled": true
273                                }
274                            }
275                        }
276                    }
277                ]
278            }
279        }"#;
280
281        assert!(validate_json_depth(json, MAX_JSON_DEPTH).is_ok());
282    }
283
284    #[test]
285    fn test_dos_payload_detection() {
286        // Test case similar to what a fuzzer might generate
287        // This creates a very deeply nested structure that could cause DoS
288        let depth = 100;
289        let mut json = String::new();
290
291        // Create nested objects
292        for i in 0..depth {
293            json.push_str(&format!("{{\"level_{}\":", i));
294        }
295        json.push_str("null");
296        for _ in 0..depth {
297            json.push('}');
298        }
299
300        let result = validate_json_depth(&json, MAX_JSON_DEPTH);
301        assert!(result.is_err());
302        assert!(
303            result
304                .expect_err("should fail on deeply nested json")
305                .contains("exceeds maximum allowed depth")
306        );
307    }
308}