Skip to content

fix: prevent StackOverflowError from deeply nested GeometryCollection in GeoJsonParser - #1699

Merged
dkhawk merged 4 commits into
googlemaps:mainfrom
ZeroX-404:fix/geojson-stack-overflow
Jul 10, 2026
Merged

dkhawk merged 4 commits into
googlemaps:mainfrom
ZeroX-404:fix/geojson-stack-overflow

Conversation

@ZeroX-404

Copy link
Copy Markdown
Contributor

Description:

## Problem
`GeoJsonParser.parseGeometry()` and `createGeometryCollection()` call each 
other recursively with no depth limit. A malicious GeoJSON file containing 
deeply nested `GeometryCollection` objects triggers a `StackOverflowError`, 
crashing any Android app that loads the file via `GeoJsonLayer`.

**Vulnerable call chain:**

parseGeometry()
→ createGeometry()
→ createGeometryCollection()
→ parseGeometry() // unbounded recursion


A PoC GeoJSON file of ~93KB with 2000 levels of nesting is sufficient 
to trigger the crash.

## Fix
- Add `MAX_GEOMETRY_DEPTH = 20` constant
- Add private `parseGeometry(JSONObject, int depth)` overload
- Pass depth counter through `createGeometry()` and `createGeometryCollection()`
- Geometries exceeding the limit are ignored with a `Log.w()` warning

## Testing
Verified that parsing a GeoJSON with 2000 nesting levels no longer 
throws `StackOverflowError` after this fix.

Add MAX_GEOMETRY_DEPTH = 20 limit to parseGeometry() to prevent
StackOverflowError when parsing malicious GeoJSON with deeply nested
GeometryCollection objects.

Geometries exceeding the depth limit are silently ignored and a
warning is logged via Log.w().
@google-cla

google-cla Bot commented Jun 7, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@ZeroX-404

Copy link
Copy Markdown
Contributor Author

Hi maintainers, could someone please review this fix? I verified that it prevents the StackOverflowError caused by deeply nested GeometryCollections.

@ZeroX-404

Copy link
Copy Markdown
Contributor Author

Hi @dkhawk, gentle follow up — could you take a look when you have a moment? Happy to adjust anything. Thanks!

Add three tests to GeoJsonParserTest to verify the fix for
StackOverflowError caused by deeply nested GeometryCollection objects
(introduced in the accompanying fix commit):

- testDeeplyNestedGeometryCollection_doesNotThrowStackOverflow:
  Ensures parsing 2000-level nesting no longer throws StackOverflowError

- testGeometryBeyondMaxDepth_returnsNull:
  Ensures geometry exceeding MAX_GEOMETRY_DEPTH (20) returns null
  instead of crashing

- testShallowNestedGeometryCollection_parsedCorrectly:
  Ensures normal shallow nesting still parses correctly (regression guard)

Relates to: googlemaps#1699
@ZeroX-404

Copy link
Copy Markdown
Contributor Author

Hi @dkhawk, I've updated the PR with unit tests covering the depth-limit behavior:

  • testDeeplyNestedGeometryCollection_doesNotThrowStackOverflow — verifies 2000-level nesting no longer crashes
  • testGeometryBeyondMaxDepth_returnsNull — verifies geometry beyond MAX_GEOMETRY_DEPTH (20) returns null
  • testShallowNestedGeometryCollection_parsedCorrectly — regression guard for normal shallow nesting

Happy to adjust anything if needed. Thanks!

@dkhawk

dkhawk commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Hi @dkhawk, I've updated the PR with unit tests covering the depth-limit behavior:

  • testDeeplyNestedGeometryCollection_doesNotThrowStackOverflow — verifies 2000-level nesting no longer crashes
  • testGeometryBeyondMaxDepth_returnsNull — verifies geometry beyond MAX_GEOMETRY_DEPTH (20) returns null
  • testShallowNestedGeometryCollection_parsedCorrectly — regression guard for normal shallow nesting

Happy to adjust anything if needed. Thanks!

Thank you for your contribution! And sorry for the delay in reviewing it as I have been away.

BTW, this version of code is slated to be replaced soon. We have a complete rewrite in https://github.com/googlemaps/android-maps-utils/tree/feat/rewrite-android-maps-utils. We have put out the first RC and expect RC2 soon.

I'll see if the problem also exists in that rewrite.

@ZeroX-404
ZeroX-404 force-pushed the fix/geojson-stack-overflow branch from 8bb2c5b to f37823d Compare June 24, 2026 22:29
@ZeroX-404

Copy link
Copy Markdown
Contributor Author

Hi @dkhawk, I've applied your suggested changes — countdown maxDepth logic and updated test assertions. All tests pass locally. Ready for merge whenever you are!

@ZeroX-404

Copy link
Copy Markdown
Contributor Author

Hi @dkhawk, thanks again for the review! Just following up — happy to wait for the CI checks to finish.

Also, regarding the rewrite branch (feat/rewrite-android-maps-utils) — did you get a chance to check if the same unbounded recursion issue exists in GeoJsonParser there? If it does, I'd be glad to open a similar PR against that branch once this one is settled, so the fix carries forward.

@dkhawk
dkhawk merged commit f513807 into googlemaps:main Jul 10, 2026
10 of 11 checks passed
agilira added a commit to agilira/android-maps-utils that referenced this pull request Jul 25, 2026
googlemaps#1699 and googlemaps#1710 added nesting-depth guards, but they run on the tree that
Json.parseToJsonElement() has already fully materialised, so they act too late:
GeoJsonParser.parse() reads and materialises the whole untrusted document
before any check. A hostile layer can therefore still

  - overflow the parser stack with deep nesting (StackOverflowError) before the
    MAX_GEOMETRY_DEPTH=20 guard runs (contrast KmlParser, which bounds depth
    *during* streaming via DepthLimitingReader);
  - exhaust the heap with a wide, shallow document (OutOfMemoryError), which no
    depth guard addresses;
  - forward non-finite coordinates ("Infinity"/"NaN", accepted by
    String.toDouble()) straight into LatLng/LatLngBounds.

The first two throw java.lang.Error subclasses, so the catch (Exception) in
DataLayerLoader does not contain them and the host app crashes on load.

Bound the input up front, before parseToJsonElement:

  - readTextBounded() caps the characters read (configurable, default 10 MiB);
  - checkStructuralDepth() rejects raw '{'/'[' nesting beyond a limit in a
    single string-aware pass (configurable, default 512 - safe on small Android
    stacks, far above any legitimate GeoJSON);
  - parseCoordinates() rejects non-finite values.

Limits are constructor parameters with safe defaults, mirroring KmzParser.
Existing behaviour is preserved (incl. the googlemaps#1699 depth-200 GeometryCollection
test); adds tests for each case.
dkhawk pushed a commit that referenced this pull request Aug 4, 2026
…ion (#1733)

* fix: bound untrusted GeoJSON input to prevent parser resource exhaustion

#1699 and #1710 added nesting-depth guards, but they run on the tree that
Json.parseToJsonElement() has already fully materialised, so they act too late:
GeoJsonParser.parse() reads and materialises the whole untrusted document
before any check. A hostile layer can therefore still

  - overflow the parser stack with deep nesting (StackOverflowError) before the
    MAX_GEOMETRY_DEPTH=20 guard runs (contrast KmlParser, which bounds depth
    *during* streaming via DepthLimitingReader);
  - exhaust the heap with a wide, shallow document (OutOfMemoryError), which no
    depth guard addresses;
  - forward non-finite coordinates ("Infinity"/"NaN", accepted by
    String.toDouble()) straight into LatLng/LatLngBounds.

The first two throw java.lang.Error subclasses, so the catch (Exception) in
DataLayerLoader does not contain them and the host app crashes on load.

Bound the input up front, before parseToJsonElement:

  - readTextBounded() caps the characters read (configurable, default 10 MiB);
  - checkStructuralDepth() rejects raw '{'/'[' nesting beyond a limit in a
    single string-aware pass (configurable, default 512 - safe on small Android
    stacks, far above any legitimate GeoJSON);
  - parseCoordinates() rejects non-finite values.

Limits are constructor parameters with safe defaults, mirroring KmzParser.
Existing behaviour is preserved (incl. the #1699 depth-200 GeometryCollection
test); adds tests for each case.

* test: make the deep-nesting test reproduce the overflow deterministically

The previous version of this test could pass on unpatched code for an
unrelated reason: with the nesting inside "coordinates", a parse that
survives the stack reaches parseCoordinates(), which calls jsonPrimitive
on a JsonArray and throws IllegalArgumentException - the very type the
test asserts.

Move the nesting under "bbox", a member the parser never dereferences,
so nothing can reject the document first, and run the parse on a thread
with an explicit 1 MB stack so the outcome does not depend on the stack
size of the thread the test framework happens to use. 50 000 levels
overflow that stack whether or not the parser is JIT-compiled, while the
structural-depth check rejects the document at nesting level 513 without
recursing at all. A small document is parsed first so that class loading
is not charged to the bounded stack.

On main this test now fails with StackOverflowError instead of passing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants