Repository navigation
fix: prevent StackOverflowError from deeply nested GeometryCollection in GeoJsonParser - #1699
Conversation
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().
|
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. |
|
Hi maintainers, could someone please review this fix? I verified that it prevents the StackOverflowError caused by deeply nested GeometryCollections. |
|
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
|
Hi @dkhawk, I've updated the PR with unit tests covering the depth-limit behavior:
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. |
…ded by dkhawk's commit
8bb2c5b to
f37823d
Compare
|
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! |
|
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. |
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.
…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.
Description:
parseGeometry()
→ createGeometry()
→ createGeometryCollection()
→ parseGeometry() // unbounded recursion