Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
2026-04-13 Todd White <todd.white@thalion.global>

* Source/NSJSONSerialization.m: cap the JSON parser recursion depth
at 512 levels to prevent stack overflow on pathologically nested
input. The parser state now tracks current nesting depth; parseArray
and parseObject bail out with a parse error when the bound is
reached, and decrement the depth counter on every return path so
the bound is correctly enforced across mixed array/object nesting.
* Tests/base/NSJSONSerialization/depth.m: new regression test that
verifies (a) moderately nested documents still parse, (b) documents
nested past the bound are rejected with the error out-parameter
populated, and (c) the bound applies equally to mixed object/array
nesting.

2026-04-08 Richard Frith-Macdonald <rfm@gnu.org>

* Headers/Foundation/NSURL.h: Add newer NSURLComponents methods for
Expand Down
52 changes: 52 additions & 0 deletions Source/NSJSONSerialization.m
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,38 @@
* Error value, if this parser is currently in an error state, nil otherwise.
*/
NSError *error;
/**
* Current nesting depth of arrays/objects on the recursive-descent
* stack. Zero before parsing begins and while the parser is outside
* any container. Incremented exactly once on successful entry to
* parseArray or parseObject (after the depth-guard check has been
* passed) and decremented exactly once on every exit path from those
* functions, including early returns for parse errors. The invariant
* that `depth` mirrors the live container stack is what lets the
* depth-guard check at the top of parseArray/parseObject reject
* pathologically nested input before the C stack is exhausted.
*/
int depth;
/**
* Upper bound on `depth`. Parsing fails with an error as soon as
* entering another container would cause `depth` to reach this value.
* Initialised by the JSONObjectWithData: and JSONObjectWithStream:
* class methods to NS_JSON_SERIALIZATION_MAX_DEPTH.
*/
int maxDepth;
} ParserState;

/**
* Default maximum JSON parser nesting depth. Any input that nests
* arrays and/or objects more than this deep is rejected before the
* recursive-descent parser can exhaust the C stack. 512 is deep enough
* to accommodate every realistic JSON document (RFC 8259 recommends
* supporting at least 100 levels; most parsers cap in the 100-1000
* range) while leaving comfortable head-room below the smallest thread
* stack that GNUstep runs on.
*/
#define NS_JSON_SERIALIZATION_MAX_DEPTH 512

/**
* Pulls the next group of characters from a string source.
*/
Expand Down Expand Up @@ -581,11 +611,17 @@
unichar c = consumeSpace(state);
NSMutableArray *array;

if (state->depth >= state->maxDepth)
{
parseError(state);
return nil;
}
if (c != '[')
{
parseError(state);
return nil;
}
state->depth++;
// Eat the [
consumeChar(state);
array = [NSMutableArray new];
Expand All @@ -597,6 +633,7 @@
if (nil == obj)
{
[array release];
state->depth--;
return nil;
}
[array addObject: obj];
Expand All @@ -610,6 +647,7 @@
}
// Eat the trailing ]
consumeChar(state);
state->depth--;
if (!state->mutableContainers)
{
if (NO == [array makeImmutable])
Expand All @@ -629,11 +667,17 @@
unichar c = consumeSpace(state);
NSMutableDictionary *dict;

if (state->depth >= state->maxDepth)
{
parseError(state);
return nil;
}
if (c != '{')
{
parseError(state);
return nil;
}
state->depth++;
// Eat the {
consumeChar(state);
dict = [NSMutableDictionary new];
Expand All @@ -646,13 +690,15 @@
if (nil == key)
{
[dict release];
state->depth--;
return nil;
}
c = consumeSpace(state);
if (':' != c)
{
[key release];
[dict release];
state->depth--;
parseError(state);
return nil;
}
Expand All @@ -663,6 +709,7 @@
{
[key release];
[dict release];
state->depth--;
return nil;
}
[dict setObject: obj forKey: key];
Expand All @@ -677,6 +724,7 @@
}
// Eat the trailing }
consumeChar(state);
state->depth--;
if (!state->mutableContainers)
{
if (NO == [dict makeImmutable])
Expand Down Expand Up @@ -1143,6 +1191,8 @@ + (id) JSONObjectWithData: (NSData *)data
ParserState p = { 0 };
id obj;

p.depth = 0;
p.maxDepth = NS_JSON_SERIALIZATION_MAX_DEPTH;
[data getBytes: BOM length: 4];
getEncoding(BOM, &p);
p.source = [[NSString alloc] initWithData: data encoding: p.enc];
Expand All @@ -1168,6 +1218,8 @@ + (id) JSONObjectWithStream: (NSInputStream *)stream
ParserState p = { 0 };
id obj;

p.depth = 0;
p.maxDepth = NS_JSON_SERIALIZATION_MAX_DEPTH;
// TODO: Handle failure here!
[stream read: (uint8_t*)BOM maxLength: 4];
getEncoding(BOM, &p);
Expand Down
Loading
Loading