From ff775b3ed28df7e7e9fff87b6ec32d7f9bf5ed55 Mon Sep 17 00:00:00 2001 From: Dale Hawkins <107309+dkhawk@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:33:46 +0000 Subject: [PATCH] fix: prevent StackOverflowError when parsing deeply nested KML containers and multi-geometries --- .../android/data/kml/KmlContainerParser.java | 17 +++++-- .../android/data/kml/KmlFeatureParser.java | 24 +++++++-- .../data/kml/KmlFeatureParserTest.java | 50 ++++++++++++++++++- .../maps/android/data/kml/KmlTestUtil.java | 12 ++++- 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/maps/android/data/kml/KmlContainerParser.java b/library/src/main/java/com/google/maps/android/data/kml/KmlContainerParser.java index 472eb31eb..8a0282ead 100644 --- a/library/src/main/java/com/google/maps/android/data/kml/KmlContainerParser.java +++ b/library/src/main/java/com/google/maps/android/data/kml/KmlContainerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google Inc. + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,10 +61,12 @@ * XmlPullParser) and assigns specific elements read from the XmlPullParser to the container. */ + /* package */ static final int MAX_CONTAINER_DEPTH = 20; + /* package */ static KmlContainer createContainer(XmlPullParser parser) throws XmlPullParserException, IOException { - return assignPropertiesToContainer(parser); + return assignPropertiesToContainer(parser, MAX_CONTAINER_DEPTH); } /** @@ -74,8 +76,12 @@ static KmlContainer createContainer(XmlPullParser parser) * @param parser XmlPullParser object reading from a KML file * @return KmlContainer object with properties read from the XmlPullParser */ - private static KmlContainer assignPropertiesToContainer(XmlPullParser parser) + /* package */ static KmlContainer assignPropertiesToContainer(XmlPullParser parser, int maxDepth) throws XmlPullParserException, IOException { + if (maxDepth < 0) { + KmlParser.skip(parser); + return null; + } String startTag = parser.getName(); String containerId = null; HashMap containerProperties = new HashMap(); @@ -97,7 +103,10 @@ private static KmlContainer assignPropertiesToContainer(XmlPullParser parser) if (parser.getName().matches(UNSUPPORTED_REGEX)) { KmlParser.skip(parser); } else if (parser.getName().matches(CONTAINER_REGEX)) { - nestedContainers.add(assignPropertiesToContainer(parser)); + KmlContainer container = assignPropertiesToContainer(parser, maxDepth - 1); + if (container != null) { + nestedContainers.add(container); + } } else if (parser.getName().matches(PROPERTY_REGEX)) { containerProperties.put(parser.getName(), parser.nextText()); } else if (parser.getName().equals(STYLE_MAP)) { diff --git a/library/src/main/java/com/google/maps/android/data/kml/KmlFeatureParser.java b/library/src/main/java/com/google/maps/android/data/kml/KmlFeatureParser.java index 967456b4e..7683a9bd0 100644 --- a/library/src/main/java/com/google/maps/android/data/kml/KmlFeatureParser.java +++ b/library/src/main/java/com/google/maps/android/data/kml/KmlFeatureParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google Inc. + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -182,8 +182,19 @@ private static String getImageUrl(XmlPullParser parser) * * @param geometryType Type of geometry object to create */ - private static Geometry createGeometry(XmlPullParser parser, String geometryType) + /* package */ static final int MAX_GEOMETRY_DEPTH = 20; + + /* package */ static Geometry createGeometry(XmlPullParser parser, String geometryType) + throws IOException, XmlPullParserException { + return createGeometry(parser, geometryType, MAX_GEOMETRY_DEPTH); + } + + /* package */ static Geometry createGeometry(XmlPullParser parser, String geometryType, int maxDepth) throws IOException, XmlPullParserException { + if (maxDepth < 0) { + KmlParser.skip(parser); + return null; + } int eventType = parser.getEventType(); while (!(eventType == END_TAG && parser.getName().equals(geometryType))) { if (eventType == START_TAG) { @@ -196,7 +207,7 @@ private static Geometry createGeometry(XmlPullParser parser, String geometryType } else if (parser.getName().equals("Polygon")) { return createPolygon(parser); } else if (parser.getName().equals("MultiGeometry")) { - return createMultiGeometry(parser); + return createMultiGeometry(parser, maxDepth); } else if (parser.getName().equals("MultiTrack")) { return createMultiTrack(parser); } @@ -349,14 +360,17 @@ private static KmlPolygon createPolygon(XmlPullParser parser) * * @return KmlMultiGeometry object */ - private static KmlMultiGeometry createMultiGeometry(XmlPullParser parser) + private static KmlMultiGeometry createMultiGeometry(XmlPullParser parser, int maxDepth) throws XmlPullParserException, IOException { ArrayList geometries = new ArrayList(); // Get next otherwise have an infinite loop int eventType = parser.next(); while (!(eventType == END_TAG && parser.getName().equals("MultiGeometry"))) { if (eventType == START_TAG && parser.getName().matches(GEOMETRY_REGEX)) { - geometries.add(createGeometry(parser, parser.getName())); + Geometry geom = createGeometry(parser, parser.getName(), maxDepth - 1); + if (geom != null) { + geometries.add(geom); + } } eventType = parser.next(); } diff --git a/library/src/test/java/com/google/maps/android/data/kml/KmlFeatureParserTest.java b/library/src/test/java/com/google/maps/android/data/kml/KmlFeatureParserTest.java index 820c91bdc..462a56840 100644 --- a/library/src/test/java/com/google/maps/android/data/kml/KmlFeatureParserTest.java +++ b/library/src/test/java/com/google/maps/android/data/kml/KmlFeatureParserTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google Inc. + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -125,4 +125,52 @@ public void testSuitableCoordinates() throws Exception { assertEquals(latLng.latitude, -43.60505741890396, 0.001); assertEquals(latLng.longitude, 170.1435558771009, 0.001); } + + @Test + public void testDeeplyNestedMultiGeometry_doesNotThrowStackOverflow() throws Exception { + StringBuilder sb = new StringBuilder(""); + for (int i = 0; i < 200; i++) { + sb.append(""); + } + sb.append("0,0"); + for (int i = 0; i < 200; i++) { + sb.append(""); + } + sb.append(""); + XmlPullParser parser = KmlTestUtil.createParserFromString(sb.toString()); + parser.next(); + KmlPlacemark placemark = KmlFeatureParser.createPlacemark(parser); + assertNotNull(placemark); + } + + @Test + public void testGeometryExceedingMaxDepth_returnsNull() throws Exception { + XmlPullParser parser = KmlTestUtil.createParserFromString("0,0"); + parser.next(); + assertNull(KmlFeatureParser.createGeometry(parser, "Point", -1)); + } + + @Test + public void testDeeplyNestedFolder_doesNotThrowStackOverflow() throws Exception { + StringBuilder sb = new StringBuilder(""); + for (int i = 0; i < 200; i++) { + sb.append(""); + } + sb.append("Deep Folder"); + for (int i = 0; i < 200; i++) { + sb.append(""); + } + sb.append(""); + XmlPullParser parser = KmlTestUtil.createParserFromString(sb.toString()); + parser.next(); + KmlContainer container = KmlContainerParser.assignPropertiesToContainer(parser, 20); + assertNotNull(container); + } + + @Test + public void testContainerExceedingMaxDepth_returnsNull() throws Exception { + XmlPullParser parser = KmlTestUtil.createParserFromString("Test"); + parser.next(); + assertNull(KmlContainerParser.assignPropertiesToContainer(parser, -1)); + } } diff --git a/library/src/test/java/com/google/maps/android/data/kml/KmlTestUtil.java b/library/src/test/java/com/google/maps/android/data/kml/KmlTestUtil.java index d603edf38..767b669b0 100644 --- a/library/src/test/java/com/google/maps/android/data/kml/KmlTestUtil.java +++ b/library/src/test/java/com/google/maps/android/data/kml/KmlTestUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google Inc. + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,4 +43,14 @@ static XmlPullParser createParser(String fileName) throws XmlPullParserException parser.setInput(stream, null); return parser; } + + static XmlPullParser createParserFromString(String xml) throws XmlPullParserException, IOException { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, false); + factory.setFeature(XmlPullParser.FEATURE_VALIDATION, false); + factory.setNamespaceAware(true); + XmlPullParser parser = factory.newPullParser(); + parser.setInput(new java.io.StringReader(xml)); + return parser; + } }