Skip to content

Commit 1463cc5

Browse files
authored
fix: prevent StackOverflowError when parsing deeply nested KML containers and multi-geometries (#1710)
1 parent d011a44 commit 1463cc5

4 files changed

Lines changed: 92 additions & 11 deletions

File tree

library/src/main/java/com/google/maps/android/data/kml/KmlContainerParser.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 Google Inc.
2+
* Copyright 2026 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -61,10 +61,12 @@
6161
* XmlPullParser) and assigns specific elements read from the XmlPullParser to the container.
6262
*/
6363

64+
/* package */ static final int MAX_CONTAINER_DEPTH = 20;
65+
6466
/* package */
6567
static KmlContainer createContainer(XmlPullParser parser)
6668
throws XmlPullParserException, IOException {
67-
return assignPropertiesToContainer(parser);
69+
return assignPropertiesToContainer(parser, MAX_CONTAINER_DEPTH);
6870
}
6971

7072
/**
@@ -74,8 +76,12 @@ static KmlContainer createContainer(XmlPullParser parser)
7476
* @param parser XmlPullParser object reading from a KML file
7577
* @return KmlContainer object with properties read from the XmlPullParser
7678
*/
77-
private static KmlContainer assignPropertiesToContainer(XmlPullParser parser)
79+
/* package */ static KmlContainer assignPropertiesToContainer(XmlPullParser parser, int maxDepth)
7880
throws XmlPullParserException, IOException {
81+
if (maxDepth < 0) {
82+
KmlParser.skip(parser);
83+
return null;
84+
}
7985
String startTag = parser.getName();
8086
String containerId = null;
8187
HashMap<String, String> containerProperties = new HashMap<String, String>();
@@ -97,7 +103,10 @@ private static KmlContainer assignPropertiesToContainer(XmlPullParser parser)
97103
if (parser.getName().matches(UNSUPPORTED_REGEX)) {
98104
KmlParser.skip(parser);
99105
} else if (parser.getName().matches(CONTAINER_REGEX)) {
100-
nestedContainers.add(assignPropertiesToContainer(parser));
106+
KmlContainer container = assignPropertiesToContainer(parser, maxDepth - 1);
107+
if (container != null) {
108+
nestedContainers.add(container);
109+
}
101110
} else if (parser.getName().matches(PROPERTY_REGEX)) {
102111
containerProperties.put(parser.getName(), parser.nextText());
103112
} else if (parser.getName().equals(STYLE_MAP)) {

library/src/main/java/com/google/maps/android/data/kml/KmlFeatureParser.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 Google Inc.
2+
* Copyright 2026 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -182,8 +182,19 @@ private static String getImageUrl(XmlPullParser parser)
182182
*
183183
* @param geometryType Type of geometry object to create
184184
*/
185-
private static Geometry createGeometry(XmlPullParser parser, String geometryType)
185+
/* package */ static final int MAX_GEOMETRY_DEPTH = 20;
186+
187+
/* package */ static Geometry createGeometry(XmlPullParser parser, String geometryType)
188+
throws IOException, XmlPullParserException {
189+
return createGeometry(parser, geometryType, MAX_GEOMETRY_DEPTH);
190+
}
191+
192+
/* package */ static Geometry createGeometry(XmlPullParser parser, String geometryType, int maxDepth)
186193
throws IOException, XmlPullParserException {
194+
if (maxDepth < 0) {
195+
KmlParser.skip(parser);
196+
return null;
197+
}
187198
int eventType = parser.getEventType();
188199
while (!(eventType == END_TAG && parser.getName().equals(geometryType))) {
189200
if (eventType == START_TAG) {
@@ -196,7 +207,7 @@ private static Geometry createGeometry(XmlPullParser parser, String geometryType
196207
} else if (parser.getName().equals("Polygon")) {
197208
return createPolygon(parser);
198209
} else if (parser.getName().equals("MultiGeometry")) {
199-
return createMultiGeometry(parser);
210+
return createMultiGeometry(parser, maxDepth);
200211
} else if (parser.getName().equals("MultiTrack")) {
201212
return createMultiTrack(parser);
202213
}
@@ -349,14 +360,17 @@ private static KmlPolygon createPolygon(XmlPullParser parser)
349360
*
350361
* @return KmlMultiGeometry object
351362
*/
352-
private static KmlMultiGeometry createMultiGeometry(XmlPullParser parser)
363+
private static KmlMultiGeometry createMultiGeometry(XmlPullParser parser, int maxDepth)
353364
throws XmlPullParserException, IOException {
354365
ArrayList<Geometry> geometries = new ArrayList<Geometry>();
355366
// Get next otherwise have an infinite loop
356367
int eventType = parser.next();
357368
while (!(eventType == END_TAG && parser.getName().equals("MultiGeometry"))) {
358369
if (eventType == START_TAG && parser.getName().matches(GEOMETRY_REGEX)) {
359-
geometries.add(createGeometry(parser, parser.getName()));
370+
Geometry geom = createGeometry(parser, parser.getName(), maxDepth - 1);
371+
if (geom != null) {
372+
geometries.add(geom);
373+
}
360374
}
361375
eventType = parser.next();
362376
}

library/src/test/java/com/google/maps/android/data/kml/KmlFeatureParserTest.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 Google Inc.
2+
* Copyright 2026 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -125,4 +125,52 @@ public void testSuitableCoordinates() throws Exception {
125125
assertEquals(latLng.latitude, -43.60505741890396, 0.001);
126126
assertEquals(latLng.longitude, 170.1435558771009, 0.001);
127127
}
128+
129+
@Test
130+
public void testDeeplyNestedMultiGeometry_doesNotThrowStackOverflow() throws Exception {
131+
StringBuilder sb = new StringBuilder("<Placemark>");
132+
for (int i = 0; i < 200; i++) {
133+
sb.append("<MultiGeometry>");
134+
}
135+
sb.append("<Point><coordinates>0,0</coordinates></Point>");
136+
for (int i = 0; i < 200; i++) {
137+
sb.append("</MultiGeometry>");
138+
}
139+
sb.append("</Placemark>");
140+
XmlPullParser parser = KmlTestUtil.createParserFromString(sb.toString());
141+
parser.next();
142+
KmlPlacemark placemark = KmlFeatureParser.createPlacemark(parser);
143+
assertNotNull(placemark);
144+
}
145+
146+
@Test
147+
public void testGeometryExceedingMaxDepth_returnsNull() throws Exception {
148+
XmlPullParser parser = KmlTestUtil.createParserFromString("<Point><coordinates>0,0</coordinates></Point>");
149+
parser.next();
150+
assertNull(KmlFeatureParser.createGeometry(parser, "Point", -1));
151+
}
152+
153+
@Test
154+
public void testDeeplyNestedFolder_doesNotThrowStackOverflow() throws Exception {
155+
StringBuilder sb = new StringBuilder("<Folder>");
156+
for (int i = 0; i < 200; i++) {
157+
sb.append("<Folder>");
158+
}
159+
sb.append("<name>Deep Folder</name>");
160+
for (int i = 0; i < 200; i++) {
161+
sb.append("</Folder>");
162+
}
163+
sb.append("</Folder>");
164+
XmlPullParser parser = KmlTestUtil.createParserFromString(sb.toString());
165+
parser.next();
166+
KmlContainer container = KmlContainerParser.assignPropertiesToContainer(parser, 20);
167+
assertNotNull(container);
168+
}
169+
170+
@Test
171+
public void testContainerExceedingMaxDepth_returnsNull() throws Exception {
172+
XmlPullParser parser = KmlTestUtil.createParserFromString("<Folder><name>Test</name></Folder>");
173+
parser.next();
174+
assertNull(KmlContainerParser.assignPropertiesToContainer(parser, -1));
175+
}
128176
}

library/src/test/java/com/google/maps/android/data/kml/KmlTestUtil.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 Google Inc.
2+
* Copyright 2026 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -43,4 +43,14 @@ static XmlPullParser createParser(String fileName) throws XmlPullParserException
4343
parser.setInput(stream, null);
4444
return parser;
4545
}
46+
47+
static XmlPullParser createParserFromString(String xml) throws XmlPullParserException, IOException {
48+
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
49+
factory.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, false);
50+
factory.setFeature(XmlPullParser.FEATURE_VALIDATION, false);
51+
factory.setNamespaceAware(true);
52+
XmlPullParser parser = factory.newPullParser();
53+
parser.setInput(new java.io.StringReader(xml));
54+
return parser;
55+
}
4656
}

0 commit comments

Comments
 (0)