Skip to content

Commit 820e9dc

Browse files
committed
Unified: Bound JSON depth
To avoid malicious JSON from taking down the extractor, we calculate the nesting depth before attempting the deserialisation. A limit of 2048 seems like it should cover our needs for the time being.
1 parent b901210 commit 820e9dc

1 file changed

Lines changed: 75 additions & 7 deletions

File tree

‎unified/extractor/src/languages/swift/adapter.rs‎

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,53 @@ const VARYING_TOKEN_KINDS: &[&str] = &[
5858
"unknown",
5959
];
6060

61+
/// Maximum structural nesting accepted in the serialized JSON tree.
62+
///
63+
/// This exceeds the deepest tree in the Swift corpus while bounding the
64+
/// recursive serde deserialization and AST construction that follow.
65+
const MAX_JSON_DEPTH: usize = 2048;
66+
67+
/// Check JSON structural depth without recursively parsing it.
68+
///
69+
/// Brackets and braces inside strings are ignored. Full JSON validation is
70+
/// still performed by serde_json afterward.
71+
fn check_json_depth(json: &str) -> Result<(), String> {
72+
let mut depth = 0;
73+
let mut in_string = false;
74+
let mut escaped = false;
75+
76+
for byte in json.bytes() {
77+
if in_string {
78+
if escaped {
79+
escaped = false;
80+
} else {
81+
match byte {
82+
b'\\' => escaped = true,
83+
b'"' => in_string = false,
84+
_ => {}
85+
}
86+
}
87+
} else {
88+
match byte {
89+
b'"' => in_string = true,
90+
b'{' | b'[' => {
91+
depth += 1;
92+
if depth > MAX_JSON_DEPTH {
93+
return Err(format!(
94+
"invalid JSON: nesting depth exceeds supported maximum \
95+
({MAX_JSON_DEPTH})"
96+
));
97+
}
98+
}
99+
b'}' | b']' => depth = depth.saturating_sub(1),
100+
_ => {}
101+
}
102+
}
103+
}
104+
105+
Ok(())
106+
}
107+
61108
/// Keys of a node object that carry metadata rather than a structural child.
62109
fn is_metadata_key(key: &str) -> bool {
63110
matches!(
@@ -326,6 +373,7 @@ const SWIFT_NODE_TYPES: &str = include_str!("../../../swift_node_types.yml");
326373
/// authoritative swift-syntax schema ([`SWIFT_NODE_TYPES`]); the adapter only
327374
/// ever consumes swift-syntax input, so the schema is not a parameter.
328375
pub fn json_to_ast(json: &str) -> Result<AdaptedTree, String> {
376+
check_json_depth(json)?;
329377
let mut deserializer = serde_json::Deserializer::from_str(json);
330378
deserializer.disable_recursion_limit();
331379
let root = Value::deserialize(&mut deserializer).map_err(|e| format!("invalid JSON: {e}"))?;
@@ -349,6 +397,14 @@ pub fn json_to_ast(json: &str) -> Result<AdaptedTree, String> {
349397
mod tests {
350398
use super::*;
351399

400+
fn deeply_nested_json(depth: usize) -> String {
401+
let mut child = r#"{"$pos":0,"$end":0,"kind":"sourceFile"}"#.to_string();
402+
for _ in 0..depth {
403+
child = format!(r#"{{"$pos":0,"$end":0,"kind":"sourceFile","child":{child}}}"#);
404+
}
405+
format!(r#"{{"$lineStarts":[0],"$pos":0,"$end":0,"kind":"sourceFile","child":{child}}}"#)
406+
}
407+
352408
/// A hand-written JSON tree exercising layout nodes, a named (varying)
353409
/// token, a fixed keyword token, and an elided collection field — so the
354410
/// adapter is tested without needing the Swift toolchain.
@@ -522,15 +578,27 @@ mod tests {
522578

523579
#[test]
524580
fn accepts_deeply_nested_json() {
525-
let mut child = r#"{"$pos":0,"$end":0,"kind":"sourceFile"}"#.to_string();
526-
for _ in 0..256 {
527-
child = format!(r#"{{"$pos":0,"$end":0,"kind":"sourceFile","child":{child}}}"#);
528-
}
529-
let json = format!(
530-
r#"{{"$lineStarts":[0],"$pos":0,"$end":0,"kind":"sourceFile","child":{child}}}"#
581+
let json = deeply_nested_json(256);
582+
json_to_ast(&json).expect("adapter should accept JSON nested beyond serde_json's default");
583+
}
584+
585+
#[test]
586+
fn rejects_json_beyond_supported_depth() {
587+
let json = deeply_nested_json(MAX_JSON_DEPTH);
588+
let error = match json_to_ast(&json) {
589+
Ok(_) => panic!("adapter should reject excessively nested JSON"),
590+
Err(error) => error,
591+
};
592+
assert!(
593+
error.contains("nesting depth exceeds supported maximum"),
594+
"{error}"
531595
);
596+
}
532597

533-
json_to_ast(&json).expect("adapter should accept JSON nested beyond serde_json's default");
598+
#[test]
599+
fn ignores_brackets_inside_json_strings_when_checking_depth() {
600+
check_json_depth(r#"{"text":"[[[{{{\\\""}"#)
601+
.expect("string contents should not contribute to JSON depth");
534602
}
535603

536604
#[test]

0 commit comments

Comments
 (0)