Skip to content

Commit f513807

Browse files
authored
fix: prevent StackOverflowError from deeply nested GeometryCollection in GeoJsonParser (#1699)
* fix: prevent StackOverflowError from deeply nested GeometryCollection 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(). * test: add unit tests for MAX_GEOMETRY_DEPTH limit in GeoJsonParser 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: #1699 * test: remove duplicate testGeometryBeyondMaxDepth_returnsNull superseded by dkhawk's commit * fix: apply dkhawk's changes - countdown maxDepth and updated tests
1 parent ea2094c commit f513807

2 files changed

Lines changed: 77 additions & 5 deletions

File tree

library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,18 @@ private static LatLngBounds parseBoundingBox(JSONArray coordinates) throws JSONE
177177
* @param geoJsonGeometry geometry object to parse
178178
* @return Geometry object
179179
*/
180+
/** Maximum nesting depth for GeometryCollection to prevent stack overflow. */
181+
private static final int MAX_GEOMETRY_DEPTH = 20;
182+
180183
public static Geometry parseGeometry(JSONObject geoJsonGeometry) {
184+
return parseGeometry(geoJsonGeometry, MAX_GEOMETRY_DEPTH);
185+
}
186+
187+
public static Geometry parseGeometry(JSONObject geoJsonGeometry, int maxDepth) {
188+
if (maxDepth < 0) {
189+
Log.w(LOG_TAG, "GeoJSON geometry nesting depth limit exhausted, ignoring.");
190+
return null;
191+
}
181192
try {
182193
String geometryType = geoJsonGeometry.getString("type");
183194
JSONArray geometryArray;
@@ -190,7 +201,7 @@ public static Geometry parseGeometry(JSONObject geoJsonGeometry) {
190201
// No geometries or coordinates array
191202
return null;
192203
}
193-
return createGeometry(geometryType, geometryArray);
204+
return createGeometry(geometryType, geometryArray, maxDepth);
194205
} catch (JSONException e) {
195206
return null;
196207
}
@@ -239,7 +250,7 @@ private static HashMap<String, String> parseProperties(JSONObject properties)
239250
* @return Geometry object
240251
* @throws JSONException if the coordinates or geometries could be parsed
241252
*/
242-
private static Geometry createGeometry(String geometryType, JSONArray geometryArray)
253+
private static Geometry createGeometry(String geometryType, JSONArray geometryArray, int maxDepth)
243254
throws JSONException {
244255
switch (geometryType) {
245256
case POINT:
@@ -255,7 +266,7 @@ private static Geometry createGeometry(String geometryType, JSONArray geometryAr
255266
case MULTIPOLYGON:
256267
return createMultiPolygon(geometryArray);
257268
case GEOMETRY_COLLECTION:
258-
return createGeometryCollection(geometryArray);
269+
return createGeometryCollection(geometryArray, maxDepth - 1);
259270
}
260271
return null;
261272
}
@@ -360,13 +371,13 @@ private static GeoJsonMultiPolygon createMultiPolygon(JSONArray coordinates)
360371
* @return GeoJsonGeometryCollection object
361372
* @throws JSONException if geometries cannot be parsed
362373
*/
363-
private static GeoJsonGeometryCollection createGeometryCollection(JSONArray geometries)
374+
private static GeoJsonGeometryCollection createGeometryCollection(JSONArray geometries, int maxDepth)
364375
throws JSONException {
365376
ArrayList<Geometry> geometryCollectionElements
366377
= new ArrayList<>();
367378
for (int i = 0; i < geometries.length(); i++) {
368379
JSONObject geometryElement = geometries.getJSONObject(i);
369-
Geometry geometry = parseGeometry(geometryElement);
380+
Geometry geometry = parseGeometry(geometryElement, maxDepth);
370381
if (geometry != null) {
371382
// Do not add geometries that could not be parsed
372383
geometryCollectionElements.add(geometry);

library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,4 +386,65 @@ public void testInvalidGeometry() throws Exception {
386386
parser = new GeoJsonParser(invalidGeometryInvalidCoordinatesString());
387387
assertEquals(0, parser.getFeatures().size());
388388
}
389+
390+
// ---------------------------------------------------------------
391+
// Tests for fix: StackOverflowError on deeply nested GeometryCollection
392+
// PR #1699
393+
// ---------------------------------------------------------------
394+
395+
private static JSONObject buildNestedGeometryCollection(int depth) throws Exception {
396+
JSONObject innermost = new JSONObject();
397+
innermost.put("type", "Point");
398+
innermost.put("coordinates", new org.json.JSONArray("[0, 0]"));
399+
400+
JSONObject current = innermost;
401+
for (int i = 0; i < depth; i++) {
402+
JSONObject wrapper = new JSONObject();
403+
wrapper.put("type", "GeometryCollection");
404+
wrapper.put("geometries", new org.json.JSONArray().put(current));
405+
current = wrapper;
406+
}
407+
return current;
408+
}
409+
410+
@Test
411+
public void testDeeplyNestedGeometryCollection_doesNotThrowStackOverflow() throws Exception {
412+
JSONObject deeplyNested = buildNestedGeometryCollection(2000);
413+
try {
414+
GeoJsonParser.parseGeometry(deeplyNested);
415+
} catch (StackOverflowError e) {
416+
throw new AssertionError(
417+
"StackOverflowError thrown for deeply nested GeometryCollection — fix did not work!", e);
418+
}
419+
}
420+
421+
422+
@Test
423+
public void testGeometryBeyondMaxDepth_returnsNull() throws Exception {
424+
JSONObject point = new JSONObject();
425+
point.put("type", "Point");
426+
point.put("coordinates", new org.json.JSONArray("[0, 0]"));
427+
Geometry result = GeoJsonParser.parseGeometry(point, -1);
428+
assertNull("Geometry exceeding max depth (countdown < 0) should be null", result);
429+
}
430+
431+
@Test
432+
public void testCustomMaxDepth_respectsLimit() throws Exception {
433+
JSONObject point = new JSONObject();
434+
point.put("type", "Point");
435+
point.put("coordinates", new org.json.JSONArray("[0, 0]"));
436+
Geometry valid = GeoJsonParser.parseGeometry(point, 0);
437+
assertNotNull("Geometry at maxDepth 0 should not be null", valid);
438+
Geometry invalid = GeoJsonParser.parseGeometry(point, -1);
439+
assertNull("Geometry at maxDepth -1 should be null", invalid);
440+
}
441+
442+
@Test
443+
public void testShallowNestedGeometryCollection_parsedCorrectly() throws Exception {
444+
JSONObject shallow = buildNestedGeometryCollection(3);
445+
Geometry result = GeoJsonParser.parseGeometry(shallow);
446+
assertNotNull("GeometryCollection with normal nesting should not be null", result);
447+
assertTrue("Geometry type must be GeoJsonGeometryCollection",
448+
result instanceof GeoJsonGeometryCollection);
449+
}
389450
}

0 commit comments

Comments
 (0)