Skip to content

Commit e022239

Browse files
authored
Merge pull request #16 from spyoungtech/fix-stack-overflow
Limit nesting depth by default to guard against stack overflow
2 parents 37684d5 + 9325c3e commit e022239

File tree

4 files changed

+88
-7
lines changed

4 files changed

+88
-7
lines changed

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ unicode-general-category = { version = "1.0" }
1717
[features]
1818
default = ["serde"]
1919
serde = ["dep:serde"]
20+
unlimited_depth = []
21+
22+
2023

2124
[dev-dependencies.serde]
2225
version = "1.0"
@@ -49,4 +52,5 @@ name = "json5-trailing-comma-formatter"
4952
test = true
5053

5154
[package.metadata.docs.rs]
52-
all-features = true
55+
all-features = true
56+

src/parser.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,19 @@ struct JSON5Parser<'toks, 'input> {
251251
source: &'input str,
252252
source_tokens: Peekable<Iter<'toks, TokenSpan>>,
253253
lookahead: Option<&'toks TokenSpan>,
254+
current_depth: usize,
255+
max_depth: usize,
254256
}
255257

256258

257259
impl<'toks, 'input> JSON5Parser<'toks, 'input> {
258260
fn new(tokens: &'toks Tokens<'input>) -> Self {
259-
JSON5Parser { source_tokens: tokens.tok_spans.iter().peekable(), lookahead: None, source: tokens.source }
261+
use crate::utils::MAX_DEPTH;
262+
JSON5Parser { source_tokens: tokens.tok_spans.iter().peekable(), lookahead: None, source: tokens.source, current_depth: 0, max_depth: MAX_DEPTH }
263+
}
264+
265+
fn with_max_depth(tokens: &'toks Tokens<'input>, max_depth: usize) -> Self {
266+
JSON5Parser { source_tokens: tokens.tok_spans.iter().peekable(), lookahead: None, source: tokens.source, current_depth: 0, max_depth: max_depth }
260267
}
261268

262269
fn advance(&mut self) -> Option<&'toks TokenSpan> {
@@ -529,7 +536,14 @@ impl<'toks, 'input> JSON5Parser<'toks, 'input> {
529536

530537

531538
fn parse_value(&mut self) -> Result<JSONValue<'input>, ParsingError> {
532-
self.parse_obj_or_array()
539+
self.current_depth = self.current_depth + 1;
540+
if self.current_depth > self.max_depth {
541+
let idx = self.position();
542+
return Err(self.make_error(format!("max depth ({}) exceeded in nested arrays/objects. To expand the depth, use the ``with_max_depth`` constructor or enable the `unlimited_depth` feature", self.max_depth), idx))
543+
}
544+
let res = self.parse_obj_or_array();
545+
self.current_depth = self.current_depth - 1;
546+
res
533547
}
534548

535549
fn parse_text(&mut self) -> Result<JSONText<'input>, ParsingError> {
@@ -606,6 +620,23 @@ mod tests {
606620
assert!(res.is_err());
607621
}
608622

623+
#[cfg(not(feature = "unlimited_depth"))]
624+
#[test]
625+
fn test_deeply_nested() {
626+
let n = 4000;
627+
let mut s = String::with_capacity(n * 2);
628+
for _ in 0 .. n {
629+
s.push('[')
630+
}
631+
for _ in 0 .. n {
632+
s.push(']')
633+
}
634+
let res = from_str(s.as_str());
635+
assert!(res.is_err());
636+
assert!(res.unwrap_err().message.contains("max depth"))
637+
}
638+
639+
609640
#[test]
610641
fn test_from_bytes() {
611642
let res = from_bytes(b"{}").unwrap();

src/rt/parser.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,12 +334,19 @@ struct JSON5Parser<'toks, 'input> {
334334
source: &'input str,
335335
source_tokens: Peekable<Iter<'toks, TokenSpan>>,
336336
lookahead: Option<&'toks TokenSpan>,
337+
current_depth: usize,
338+
max_depth: usize,
337339
}
338340

339341

340342
impl<'toks, 'input> JSON5Parser<'toks, 'input> {
341343
fn new(tokens: &'toks Tokens<'input>) -> Self {
342-
JSON5Parser { source_tokens: tokens.tok_spans.iter().peekable(), lookahead: None, source: tokens.source }
344+
use crate::utils::MAX_DEPTH;
345+
JSON5Parser { source_tokens: tokens.tok_spans.iter().peekable(), lookahead: None, source: tokens.source, current_depth: 0, max_depth: MAX_DEPTH }
346+
}
347+
348+
fn with_max_depth(&mut self, tokens: &'toks Tokens<'input>, max_depth: usize) -> Self {
349+
JSON5Parser { source_tokens: tokens.tok_spans.iter().peekable(), lookahead: None, source: tokens.source, current_depth: 0, max_depth: max_depth }
343350
}
344351

345352
fn advance(&mut self) -> Option<&'toks TokenSpan> {
@@ -611,7 +618,6 @@ impl<'toks, 'input> JSON5Parser<'toks, 'input> {
611618
return Err(self.make_error(format!("Unary operations not allowed for value {:?}", val), span.2))
612619
}
613620
}
614-
615621
Ok(JSONValue::Unary {operator: UnaryOperator::Plus, value: Box::new(value)})
616622
}
617623
TokType::Minus => {
@@ -645,7 +651,14 @@ impl<'toks, 'input> JSON5Parser<'toks, 'input> {
645651

646652

647653
fn parse_value(&mut self) -> Result<JSONValue, ParsingError> {
648-
self.parse_obj_or_array()
654+
self.current_depth = self.current_depth + 1;
655+
if self.current_depth > self.max_depth {
656+
let idx = self.position();
657+
return Err(self.make_error(format!("max depth ({}) exceeded in nested arrays/objects. To expand the depth, use the ``with_max_depth`` constructor or enable the `unlimited_depth` feature", self.max_depth), idx))
658+
}
659+
let res = self.parse_obj_or_array();
660+
self.current_depth = self.current_depth - 1;
661+
res
649662
}
650663

651664
fn parse_text(&mut self) -> Result<JSONText, ParsingError> {
@@ -713,6 +726,22 @@ mod tests {
713726
assert!(res.is_err());
714727
}
715728

729+
#[cfg(not(feature = "unlimited_depth"))]
730+
#[test]
731+
fn test_deeply_nested() {
732+
let n = 4000;
733+
let mut s = String::with_capacity(n * 2);
734+
for _ in 0 .. n {
735+
s.push('[')
736+
}
737+
for _ in 0 .. n {
738+
s.push(']')
739+
}
740+
let res = crate::parser::from_str(s.as_str());
741+
assert!(res.is_err());
742+
assert!(res.unwrap_err().message.contains("max depth"))
743+
}
744+
716745
#[test]
717746
fn test_foo() {
718747
let res = from_str("{}").unwrap();

src/utils.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,21 @@ fn char_from_u32(u: u32) -> Result<char, String> {
158158

159159
fn err<S: Into<String>>(message: S) -> String {
160160
message.into()
161-
}
161+
}
162+
163+
164+
#[cfg(all(not(feature = "unlimited_depth"), not(target_os = "windows"), debug_assertions))]
165+
pub (crate) const MAX_DEPTH: usize = 1000;
166+
167+
#[cfg(all(not(feature = "unlimited_depth"), not(target_os = "windows"), not(debug_assertions)))]
168+
pub (crate) const MAX_DEPTH: usize = 3000;
169+
170+
171+
#[cfg(all(not(feature = "unlimited_depth"), target_os = "windows", debug_assertions))]
172+
pub (crate) const MAX_DEPTH: usize = 700;
173+
174+
#[cfg(all(not(feature = "unlimited_depth"), target_os = "windows", not(debug_assertions)))]
175+
pub (crate) const MAX_DEPTH: usize = 2000;
176+
177+
#[cfg(feature = "unlimited_depth")]
178+
pub (crate) const MAX_DEPTH: usize = usize::MAX;

0 commit comments

Comments
 (0)