Skip to content

Commit bae3455

Browse files
authored
fix: KMZ zip bomb mitigation by adding entry and size limits (#1677)
* fix: KMZ zip bomb mitigation by adding entry and size limits * docs: license * feat: make KMZ zip bomb limits configurable
1 parent 43855e9 commit bae3455

2 files changed

Lines changed: 305 additions & 10 deletions

File tree

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

Lines changed: 213 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,12 @@
4747
*/
4848
public class KmlLayer extends Layer {
4949

50+
private static final int DEFAULT_MAX_KMZ_ENTRY_COUNT = 200;
51+
private static final long DEFAULT_MAX_KMZ_UNCOMPRESSED_TOTAL_SIZE = 50 * 1024 * 1024; // 50MB
52+
5053
/**
5154
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
52-
*
55+
* <p>
5356
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
5457
*
5558
* @param map GoogleMap object
@@ -60,12 +63,30 @@ public class KmlLayer extends Layer {
6063
*/
6164
public KmlLayer(GoogleMap map, int resourceId, Context context)
6265
throws XmlPullParserException, IOException {
63-
this(map, context.getResources().openRawResource(resourceId), context, new MarkerManager(map), new PolygonManager(map), new PolylineManager(map), new GroundOverlayManager(map), null, null);
66+
this(map, context.getResources().openRawResource(resourceId), context, new MarkerManager(map), new PolygonManager(map), new PolylineManager(map), new GroundOverlayManager(map), null, null, DEFAULT_MAX_KMZ_ENTRY_COUNT, DEFAULT_MAX_KMZ_UNCOMPRESSED_TOTAL_SIZE);
6467
}
6568

6669
/**
6770
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
71+
* <p>
72+
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
6873
*
74+
* @param map GoogleMap object
75+
* @param resourceId Raw resource KML or KMZ file
76+
* @param context The Context
77+
* @param maxKmzEntryCount The maximum number of entries a KMZ file can contain.
78+
* @param maxKmzUncompressedTotalSize The maximum size of the uncompressed KMZ file in bytes.
79+
* @throws XmlPullParserException if file cannot be parsed
80+
* @throws IOException if I/O error
81+
*/
82+
public KmlLayer(GoogleMap map, int resourceId, Context context, int maxKmzEntryCount, long maxKmzUncompressedTotalSize)
83+
throws XmlPullParserException, IOException {
84+
this(map, context.getResources().openRawResource(resourceId), context, new MarkerManager(map), new PolygonManager(map), new PolylineManager(map), new GroundOverlayManager(map), null, null, maxKmzEntryCount, maxKmzUncompressedTotalSize);
85+
}
86+
87+
/**
88+
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
89+
* <p>
6990
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
7091
*
7192
* @param map GoogleMap object
@@ -76,14 +97,32 @@ public KmlLayer(GoogleMap map, int resourceId, Context context)
7697
*/
7798
public KmlLayer(GoogleMap map, InputStream stream, Context context)
7899
throws XmlPullParserException, IOException {
79-
this(map, stream, context, new MarkerManager(map), new PolygonManager(map), new PolylineManager(map), new GroundOverlayManager(map), null, null);
100+
this(map, stream, context, new MarkerManager(map), new PolygonManager(map), new PolylineManager(map), new GroundOverlayManager(map), null, null, DEFAULT_MAX_KMZ_ENTRY_COUNT, DEFAULT_MAX_KMZ_UNCOMPRESSED_TOTAL_SIZE);
80101
}
81102

82103
/**
83104
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
84-
*
105+
* <p>
85106
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
86107
*
108+
* @param map GoogleMap object
109+
* @param stream InputStream containing KML or KMZ file
110+
* @param context The Context
111+
* @param maxKmzEntryCount The maximum number of entries a KMZ file can contain.
112+
* @param maxKmzUncompressedTotalSize The maximum size of the uncompressed KMZ file in bytes.
113+
* @throws XmlPullParserException if file cannot be parsed
114+
* @throws IOException if I/O error
115+
*/
116+
public KmlLayer(GoogleMap map, InputStream stream, Context context, int maxKmzEntryCount, long maxKmzUncompressedTotalSize)
117+
throws XmlPullParserException, IOException {
118+
this(map, stream, context, new MarkerManager(map), new PolygonManager(map), new PolylineManager(map), new GroundOverlayManager(map), null, null, maxKmzEntryCount, maxKmzUncompressedTotalSize);
119+
}
120+
121+
/**
122+
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
123+
* <p>
124+
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
125+
* <p>
87126
* Use this constructor with shared object managers in order to handle multiple layers with
88127
* their own event handlers on the map.
89128
*
@@ -107,14 +146,49 @@ public KmlLayer(GoogleMap map,
107146
GroundOverlayManager groundOverlayManager,
108147
Renderer.ImagesCache cache)
109148
throws XmlPullParserException, IOException {
110-
this(map, context.getResources().openRawResource(resourceId), context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache, null);
149+
this(map, context.getResources().openRawResource(resourceId), context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache, null, DEFAULT_MAX_KMZ_ENTRY_COUNT, DEFAULT_MAX_KMZ_UNCOMPRESSED_TOTAL_SIZE);
111150
}
112151

113152
/**
114153
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
115-
*
154+
* <p>
116155
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
156+
* <p>
157+
* Use this constructor with shared object managers in order to handle multiple layers with
158+
* their own event handlers on the map.
117159
*
160+
* @param map GoogleMap object
161+
* @param resourceId Raw resource KML or KMZ file
162+
* @param context The Context
163+
* @param markerManager marker manager to create marker collection from
164+
* @param polygonManager polygon manager to create polygon collection from
165+
* @param polylineManager polyline manager to create polyline collection from
166+
* @param groundOverlayManager ground overlay manager to create ground overlay collection from
167+
* @param cache cache to be used for fetched images
168+
* @param maxKmzEntryCount The maximum number of entries a KMZ file can contain.
169+
* @param maxKmzUncompressedTotalSize The maximum size of the uncompressed KMZ file in bytes.
170+
* @throws XmlPullParserException if file cannot be parsed
171+
* @throws IOException if I/O error
172+
*/
173+
public KmlLayer(GoogleMap map,
174+
@RawRes int resourceId,
175+
Context context,
176+
MarkerManager markerManager,
177+
PolygonManager polygonManager,
178+
PolylineManager polylineManager,
179+
GroundOverlayManager groundOverlayManager,
180+
Renderer.ImagesCache cache,
181+
int maxKmzEntryCount,
182+
long maxKmzUncompressedTotalSize)
183+
throws XmlPullParserException, IOException {
184+
this(map, context.getResources().openRawResource(resourceId), context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache, null, maxKmzEntryCount, maxKmzUncompressedTotalSize);
185+
}
186+
187+
/**
188+
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
189+
* <p>
190+
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
191+
* <p>
118192
* Use this constructor with shared object managers in order to handle multiple layers with
119193
* their own event handlers on the map.
120194
*
@@ -138,14 +212,49 @@ public KmlLayer(GoogleMap map,
138212
GroundOverlayManager groundOverlayManager,
139213
Renderer.ImagesCache cache)
140214
throws XmlPullParserException, IOException {
141-
this(map, stream, context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache, null);
215+
this(map, stream, context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache, null, DEFAULT_MAX_KMZ_ENTRY_COUNT, DEFAULT_MAX_KMZ_UNCOMPRESSED_TOTAL_SIZE);
142216
}
143217

144218
/**
145219
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
146-
*
220+
* <p>
147221
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
222+
* <p>
223+
* Use this constructor with shared object managers in order to handle multiple layers with
224+
* their own event handlers on the map.
148225
*
226+
* @param map GoogleMap object
227+
* @param stream InputStream containing KML or KMZ file
228+
* @param context The Context
229+
* @param markerManager marker manager to create marker collection from
230+
* @param polygonManager polygon manager to create polygon collection from
231+
* @param polylineManager polyline manager to create polyline collection from
232+
* @param groundOverlayManager ground overlay manager to create ground overlay collection from
233+
* @param cache cache to be used for fetched images
234+
* @param maxKmzEntryCount The maximum number of entries a KMZ file can contain.
235+
* @param maxKmzUncompressedTotalSize The maximum size of the uncompressed KMZ file in bytes.
236+
* @throws XmlPullParserException if file cannot be parsed
237+
* @throws IOException if I/O error
238+
*/
239+
public KmlLayer(GoogleMap map,
240+
InputStream stream,
241+
Context context,
242+
MarkerManager markerManager,
243+
PolygonManager polygonManager,
244+
PolylineManager polylineManager,
245+
GroundOverlayManager groundOverlayManager,
246+
Renderer.ImagesCache cache,
247+
int maxKmzEntryCount,
248+
long maxKmzUncompressedTotalSize)
249+
throws XmlPullParserException, IOException {
250+
this(map, stream, context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache, null, maxKmzEntryCount, maxKmzUncompressedTotalSize);
251+
}
252+
253+
/**
254+
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
255+
* <p>
256+
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
257+
* <p>
149258
* Use this constructor with shared object managers in order to handle multiple layers with
150259
* their own event handlers on the map.
151260
*
@@ -171,6 +280,43 @@ public KmlLayer(GoogleMap map,
171280
Renderer.ImagesCache cache,
172281
@Nullable KmlUrlSanitizer urlSanitizer)
173282
throws XmlPullParserException, IOException {
283+
this(map, stream, context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache, urlSanitizer, DEFAULT_MAX_KMZ_ENTRY_COUNT, DEFAULT_MAX_KMZ_UNCOMPRESSED_TOTAL_SIZE);
284+
}
285+
286+
/**
287+
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
288+
* <p>
289+
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
290+
* <p>
291+
* Use this constructor with shared object managers in order to handle multiple layers with
292+
* their own event handlers on the map.
293+
*
294+
* @param map GoogleMap object
295+
* @param stream InputStream containing KML or KMZ file
296+
* @param context The Context
297+
* @param markerManager marker manager to create marker collection from
298+
* @param polygonManager polygon manager to create polygon collection from
299+
* @param polylineManager polyline manager to create polyline collection from
300+
* @param groundOverlayManager ground overlay manager to create ground overlay collection from
301+
* @param cache cache to be used for fetched images
302+
* @param urlSanitizer sanitizer to be used for external URLs
303+
* @param maxKmzEntryCount The maximum number of entries a KMZ file can contain.
304+
* @param maxKmzUncompressedTotalSize The maximum size of the uncompressed KMZ file in bytes.
305+
* @throws XmlPullParserException if file cannot be parsed
306+
* @throws IOException if I/O error
307+
*/
308+
public KmlLayer(GoogleMap map,
309+
InputStream stream,
310+
Context context,
311+
MarkerManager markerManager,
312+
PolygonManager polygonManager,
313+
PolylineManager polylineManager,
314+
GroundOverlayManager groundOverlayManager,
315+
Renderer.ImagesCache cache,
316+
@Nullable KmlUrlSanitizer urlSanitizer,
317+
int maxKmzEntryCount,
318+
long maxKmzUncompressedTotalSize)
319+
throws XmlPullParserException, IOException {
174320
if (stream == null) {
175321
throw new IllegalArgumentException("KML InputStream cannot be null");
176322
}
@@ -179,16 +325,22 @@ public KmlLayer(GoogleMap map,
179325
BufferedInputStream bis = new BufferedInputStream(stream);
180326
bis.mark(1024);
181327
ZipInputStream zip = new ZipInputStream(bis);
328+
CountingInputStream countingStream = new CountingInputStream(zip, maxKmzUncompressedTotalSize);
182329
try {
183330
KmlParser parser = null;
184331
ZipEntry entry = zip.getNextEntry();
185332
if (entry != null) { // is a KMZ zip file
333+
int entryCount = 0;
186334
HashMap<String, Bitmap> images = new HashMap<>();
187335
while (entry != null) {
336+
entryCount++;
337+
if (entryCount > maxKmzEntryCount) {
338+
throw new IOException("Zip bomb detected! Max number of entries exceeded: " + maxKmzEntryCount);
339+
}
188340
if (parser == null && entry.getName().toLowerCase().endsWith(".kml")) {
189-
parser = parseKml(zip);
341+
parser = parseKml(countingStream);
190342
} else {
191-
Bitmap bitmap = BitmapFactory.decodeStream(zip);
343+
Bitmap bitmap = BitmapFactory.decodeStream(countingStream);
192344
if (bitmap != null) {
193345
images.put(entry.getName(), bitmap);
194346
} else {
@@ -216,6 +368,57 @@ public KmlLayer(GoogleMap map,
216368
}
217369
}
218370

371+
/**
372+
* Wrapper for an InputStream that counts the number of bytes read and throws an IOException
373+
* if the limit is exceeded.
374+
*/
375+
private static class CountingInputStream extends InputStream {
376+
private final InputStream mIn;
377+
private long mTotalBytes = 0;
378+
private final long mMaxBytes;
379+
380+
public CountingInputStream(InputStream in, long maxBytes) {
381+
mIn = in;
382+
mMaxBytes = maxBytes;
383+
}
384+
385+
@Override
386+
public int read() throws IOException {
387+
int b = mIn.read();
388+
if (b != -1) {
389+
mTotalBytes++;
390+
checkLimit();
391+
}
392+
return b;
393+
}
394+
395+
@Override
396+
public int read(byte[] b, int off, int len) throws IOException {
397+
int n = mIn.read(b, off, len);
398+
if (n != -1) {
399+
mTotalBytes += n;
400+
checkLimit();
401+
}
402+
return n;
403+
}
404+
405+
@Override
406+
public long skip(long n) throws IOException {
407+
long skipped = mIn.skip(n);
408+
if (skipped > 0) {
409+
mTotalBytes += skipped;
410+
checkLimit();
411+
}
412+
return skipped;
413+
}
414+
415+
private void checkLimit() throws IOException {
416+
if (mTotalBytes > mMaxBytes) {
417+
throw new IOException("Zip bomb detected! Uncompressed size exceeds limit of " + mMaxBytes + " bytes.");
418+
}
419+
}
420+
}
421+
219422
private static KmlParser parseKml(InputStream stream) throws XmlPullParserException, IOException {
220423
XmlPullParser xmlPullParser = createXmlParser(stream);
221424
KmlParser parser = new KmlParser(xmlPullParser);

0 commit comments

Comments
 (0)