From 0b04ac526031dbf74f11c23f2f6de957039ffe6c Mon Sep 17 00:00:00 2001 From: ZeroX-404 Date: Sun, 7 Jun 2026 07:41:01 +0000 Subject: [PATCH 1/4] 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(). --- .../android/data/geojson/GeoJsonParser.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java b/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java index 9685c5869..663a8f1a8 100644 --- a/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java +++ b/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java @@ -177,7 +177,18 @@ private static LatLngBounds parseBoundingBox(JSONArray coordinates) throws JSONE * @param geoJsonGeometry geometry object to parse * @return Geometry object */ + /** Maximum nesting depth for GeometryCollection to prevent stack overflow. */ + private static final int MAX_GEOMETRY_DEPTH = 20; + public static Geometry parseGeometry(JSONObject geoJsonGeometry) { + return parseGeometry(geoJsonGeometry, 0); + } + + private static Geometry parseGeometry(JSONObject geoJsonGeometry, int depth) { + if (depth > MAX_GEOMETRY_DEPTH) { + Log.w(LOG_TAG, "GeoJSON geometry nesting depth exceeds maximum (" + MAX_GEOMETRY_DEPTH + "), ignoring."); + return null; + } try { String geometryType = geoJsonGeometry.getString("type"); JSONArray geometryArray; @@ -190,7 +201,7 @@ public static Geometry parseGeometry(JSONObject geoJsonGeometry) { // No geometries or coordinates array return null; } - return createGeometry(geometryType, geometryArray); + return createGeometry(geometryType, geometryArray, depth); } catch (JSONException e) { return null; } @@ -239,7 +250,7 @@ private static HashMap parseProperties(JSONObject properties) * @return Geometry object * @throws JSONException if the coordinates or geometries could be parsed */ - private static Geometry createGeometry(String geometryType, JSONArray geometryArray) + private static Geometry createGeometry(String geometryType, JSONArray geometryArray, int depth) throws JSONException { switch (geometryType) { case POINT: @@ -255,7 +266,7 @@ private static Geometry createGeometry(String geometryType, JSONArray geometryAr case MULTIPOLYGON: return createMultiPolygon(geometryArray); case GEOMETRY_COLLECTION: - return createGeometryCollection(geometryArray); + return createGeometryCollection(geometryArray, depth + 1); } return null; } @@ -360,13 +371,13 @@ private static GeoJsonMultiPolygon createMultiPolygon(JSONArray coordinates) * @return GeoJsonGeometryCollection object * @throws JSONException if geometries cannot be parsed */ - private static GeoJsonGeometryCollection createGeometryCollection(JSONArray geometries) + private static GeoJsonGeometryCollection createGeometryCollection(JSONArray geometries, int depth) throws JSONException { ArrayList geometryCollectionElements = new ArrayList<>(); for (int i = 0; i < geometries.length(); i++) { JSONObject geometryElement = geometries.getJSONObject(i); - Geometry geometry = parseGeometry(geometryElement); + Geometry geometry = parseGeometry(geometryElement, depth); if (geometry != null) { // Do not add geometries that could not be parsed geometryCollectionElements.add(geometry); From bef0b52f4d43b8cbf4fb5243439b6f91649aab38 Mon Sep 17 00:00:00 2001 From: ZeroX-404 Date: Wed, 24 Jun 2026 15:01:08 +0700 Subject: [PATCH 2/4] 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 --- .../data/geojson/GeoJsonParserTest.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java b/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java index a4ada21e3..faea71978 100644 --- a/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java +++ b/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java @@ -386,4 +386,51 @@ public void testInvalidGeometry() throws Exception { parser = new GeoJsonParser(invalidGeometryInvalidCoordinatesString()); assertEquals(0, parser.getFeatures().size()); } + + // --------------------------------------------------------------- + // Tests for fix: StackOverflowError on deeply nested GeometryCollection + // PR #1699 + // --------------------------------------------------------------- + + private static JSONObject buildNestedGeometryCollection(int depth) throws Exception { + JSONObject innermost = new JSONObject(); + innermost.put("type", "Point"); + innermost.put("coordinates", new org.json.JSONArray("[0, 0]")); + + JSONObject current = innermost; + for (int i = 0; i < depth; i++) { + JSONObject wrapper = new JSONObject(); + wrapper.put("type", "GeometryCollection"); + wrapper.put("geometries", new org.json.JSONArray().put(current)); + current = wrapper; + } + return current; + } + + @Test + public void testDeeplyNestedGeometryCollection_doesNotThrowStackOverflow() throws Exception { + JSONObject deeplyNested = buildNestedGeometryCollection(2000); + try { + GeoJsonParser.parseGeometry(deeplyNested); + } catch (StackOverflowError e) { + throw new AssertionError( + "StackOverflowError thrown for deeply nested GeometryCollection — fix tidak bekerja!", e); + } + } + + @Test + public void testGeometryBeyondMaxDepth_returnsNull() throws Exception { + JSONObject tooDeep = buildNestedGeometryCollection(21); + Geometry result = GeoJsonParser.parseGeometry(tooDeep); + assertNull("Geometry melebihi MAX_GEOMETRY_DEPTH seharusnya null", result); + } + + @Test + public void testShallowNestedGeometryCollection_parsedCorrectly() throws Exception { + JSONObject shallow = buildNestedGeometryCollection(3); + Geometry result = GeoJsonParser.parseGeometry(shallow); + assertNotNull("GeometryCollection dengan nesting normal seharusnya tidak null", result); + assertTrue("Tipe geometry harus GeoJsonGeometryCollection", + result instanceof GeoJsonGeometryCollection); + } } From f37823d179d4cb20669674ba63ede96c0aabae57 Mon Sep 17 00:00:00 2001 From: ZeroX-404 Date: Thu, 25 Jun 2026 05:25:55 +0700 Subject: [PATCH 3/4] test: remove duplicate testGeometryBeyondMaxDepth_returnsNull superseded by dkhawk's commit --- .../google/maps/android/data/geojson/GeoJsonParserTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java b/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java index faea71978..cd0688348 100644 --- a/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java +++ b/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java @@ -418,12 +418,6 @@ public void testDeeplyNestedGeometryCollection_doesNotThrowStackOverflow() throw } } - @Test - public void testGeometryBeyondMaxDepth_returnsNull() throws Exception { - JSONObject tooDeep = buildNestedGeometryCollection(21); - Geometry result = GeoJsonParser.parseGeometry(tooDeep); - assertNull("Geometry melebihi MAX_GEOMETRY_DEPTH seharusnya null", result); - } @Test public void testShallowNestedGeometryCollection_parsedCorrectly() throws Exception { From 1a7f7f18ba4cec1d56d3f0ed9629c299879fa7a5 Mon Sep 17 00:00:00 2001 From: ZeroX-404 Date: Thu, 25 Jun 2026 05:52:27 +0700 Subject: [PATCH 4/4] fix: apply dkhawk's changes - countdown maxDepth and updated tests --- .../android/data/geojson/GeoJsonParser.java | 18 ++++++------- .../data/geojson/GeoJsonParserTest.java | 26 ++++++++++++++++--- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java b/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java index 663a8f1a8..ad0e07744 100644 --- a/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java +++ b/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java @@ -181,12 +181,12 @@ private static LatLngBounds parseBoundingBox(JSONArray coordinates) throws JSONE private static final int MAX_GEOMETRY_DEPTH = 20; public static Geometry parseGeometry(JSONObject geoJsonGeometry) { - return parseGeometry(geoJsonGeometry, 0); + return parseGeometry(geoJsonGeometry, MAX_GEOMETRY_DEPTH); } - private static Geometry parseGeometry(JSONObject geoJsonGeometry, int depth) { - if (depth > MAX_GEOMETRY_DEPTH) { - Log.w(LOG_TAG, "GeoJSON geometry nesting depth exceeds maximum (" + MAX_GEOMETRY_DEPTH + "), ignoring."); + public static Geometry parseGeometry(JSONObject geoJsonGeometry, int maxDepth) { + if (maxDepth < 0) { + Log.w(LOG_TAG, "GeoJSON geometry nesting depth limit exhausted, ignoring."); return null; } try { @@ -201,7 +201,7 @@ private static Geometry parseGeometry(JSONObject geoJsonGeometry, int depth) { // No geometries or coordinates array return null; } - return createGeometry(geometryType, geometryArray, depth); + return createGeometry(geometryType, geometryArray, maxDepth); } catch (JSONException e) { return null; } @@ -250,7 +250,7 @@ private static HashMap parseProperties(JSONObject properties) * @return Geometry object * @throws JSONException if the coordinates or geometries could be parsed */ - private static Geometry createGeometry(String geometryType, JSONArray geometryArray, int depth) + private static Geometry createGeometry(String geometryType, JSONArray geometryArray, int maxDepth) throws JSONException { switch (geometryType) { case POINT: @@ -266,7 +266,7 @@ private static Geometry createGeometry(String geometryType, JSONArray geometryAr case MULTIPOLYGON: return createMultiPolygon(geometryArray); case GEOMETRY_COLLECTION: - return createGeometryCollection(geometryArray, depth + 1); + return createGeometryCollection(geometryArray, maxDepth - 1); } return null; } @@ -371,13 +371,13 @@ private static GeoJsonMultiPolygon createMultiPolygon(JSONArray coordinates) * @return GeoJsonGeometryCollection object * @throws JSONException if geometries cannot be parsed */ - private static GeoJsonGeometryCollection createGeometryCollection(JSONArray geometries, int depth) + private static GeoJsonGeometryCollection createGeometryCollection(JSONArray geometries, int maxDepth) throws JSONException { ArrayList geometryCollectionElements = new ArrayList<>(); for (int i = 0; i < geometries.length(); i++) { JSONObject geometryElement = geometries.getJSONObject(i); - Geometry geometry = parseGeometry(geometryElement, depth); + Geometry geometry = parseGeometry(geometryElement, maxDepth); if (geometry != null) { // Do not add geometries that could not be parsed geometryCollectionElements.add(geometry); diff --git a/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java b/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java index cd0688348..3529beebe 100644 --- a/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java +++ b/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java @@ -414,17 +414,37 @@ public void testDeeplyNestedGeometryCollection_doesNotThrowStackOverflow() throw GeoJsonParser.parseGeometry(deeplyNested); } catch (StackOverflowError e) { throw new AssertionError( - "StackOverflowError thrown for deeply nested GeometryCollection — fix tidak bekerja!", e); + "StackOverflowError thrown for deeply nested GeometryCollection — fix did not work!", e); } } @Test + public void testGeometryBeyondMaxDepth_returnsNull() throws Exception { + JSONObject point = new JSONObject(); + point.put("type", "Point"); + point.put("coordinates", new org.json.JSONArray("[0, 0]")); + Geometry result = GeoJsonParser.parseGeometry(point, -1); + assertNull("Geometry exceeding max depth (countdown < 0) should be null", result); + } + + @Test + public void testCustomMaxDepth_respectsLimit() throws Exception { + JSONObject point = new JSONObject(); + point.put("type", "Point"); + point.put("coordinates", new org.json.JSONArray("[0, 0]")); + Geometry valid = GeoJsonParser.parseGeometry(point, 0); + assertNotNull("Geometry at maxDepth 0 should not be null", valid); + Geometry invalid = GeoJsonParser.parseGeometry(point, -1); + assertNull("Geometry at maxDepth -1 should be null", invalid); + } + + @Test public void testShallowNestedGeometryCollection_parsedCorrectly() throws Exception { JSONObject shallow = buildNestedGeometryCollection(3); Geometry result = GeoJsonParser.parseGeometry(shallow); - assertNotNull("GeometryCollection dengan nesting normal seharusnya tidak null", result); - assertTrue("Tipe geometry harus GeoJsonGeometryCollection", + assertNotNull("GeometryCollection with normal nesting should not be null", result); + assertTrue("Geometry type must be GeoJsonGeometryCollection", result instanceof GeoJsonGeometryCollection); } }