Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import android.graphics.BitmapFactory;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.annotation.RawRes;

import com.google.android.gms.maps.GoogleMap;
Expand Down Expand Up @@ -59,7 +60,7 @@ public class KmlLayer extends Layer {
*/
public KmlLayer(GoogleMap map, int resourceId, Context context)
throws XmlPullParserException, IOException {
this(map, context.getResources().openRawResource(resourceId), context, new MarkerManager(map), new PolygonManager(map), new PolylineManager(map), new GroundOverlayManager(map), null);
this(map, context.getResources().openRawResource(resourceId), context, new MarkerManager(map), new PolygonManager(map), new PolylineManager(map), new GroundOverlayManager(map), null, null);
}

/**
Expand All @@ -75,7 +76,7 @@ public KmlLayer(GoogleMap map, int resourceId, Context context)
*/
public KmlLayer(GoogleMap map, InputStream stream, Context context)
throws XmlPullParserException, IOException {
this(map, stream, context, new MarkerManager(map), new PolygonManager(map), new PolylineManager(map), new GroundOverlayManager(map), null);
this(map, stream, context, new MarkerManager(map), new PolygonManager(map), new PolylineManager(map), new GroundOverlayManager(map), null, null);
}

/**
Expand Down Expand Up @@ -106,7 +107,7 @@ public KmlLayer(GoogleMap map,
GroundOverlayManager groundOverlayManager,
Renderer.ImagesCache cache)
throws XmlPullParserException, IOException {
this(map, context.getResources().openRawResource(resourceId), context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache);
this(map, context.getResources().openRawResource(resourceId), context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache, null);
}

/**
Expand Down Expand Up @@ -137,10 +138,43 @@ public KmlLayer(GoogleMap map,
GroundOverlayManager groundOverlayManager,
Renderer.ImagesCache cache)
throws XmlPullParserException, IOException {
this(map, stream, context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache, null);
}

/**
* Creates a new KmlLayer object - addLayerToMap() must be called to trigger rendering onto a map.
*
* Constructor may be called on a background thread, as I/O and parsing may be long-running.
*
* Use this constructor with shared object managers in order to handle multiple layers with
* their own event handlers on the map.
*
* @param map GoogleMap object
* @param stream InputStream containing KML or KMZ file
* @param context The Context
* @param markerManager marker manager to create marker collection from
* @param polygonManager polygon manager to create polygon collection from
* @param polylineManager polyline manager to create polyline collection from
* @param groundOverlayManager ground overlay manager to create ground overlay collection from
* @param cache cache to be used for fetched images
* @param urlSanitizer sanitizer to be used for external URLs
* @throws XmlPullParserException if file cannot be parsed
* @throws IOException if I/O error
*/
public KmlLayer(GoogleMap map,
InputStream stream,
Context context,
MarkerManager markerManager,
PolygonManager polygonManager,
PolylineManager polylineManager,
GroundOverlayManager groundOverlayManager,
Renderer.ImagesCache cache,
@Nullable KmlUrlSanitizer urlSanitizer)
throws XmlPullParserException, IOException {
if (stream == null) {
throw new IllegalArgumentException("KML InputStream cannot be null");
}
KmlRenderer renderer = new KmlRenderer(map, context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache);
KmlRenderer renderer = new KmlRenderer(map, context, markerManager, polygonManager, polylineManager, groundOverlayManager, cache, urlSanitizer);

BufferedInputStream bis = new BufferedInputStream(stream);
bis.mark(1024);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,36 @@ public class KmlRenderer extends Renderer {

private ArrayList<KmlContainer> mContainers;

private final KmlUrlSanitizer mUrlSanitizer;

/* package */ KmlRenderer(GoogleMap map,
Context context,
MarkerManager markerManager,
PolygonManager polygonManager,
PolylineManager polylineManager,
GroundOverlayManager groundOverlayManager,
@Nullable ImagesCache imagesCache) {
@Nullable ImagesCache imagesCache,
@Nullable KmlUrlSanitizer urlSanitizer) {
super(map, context, markerManager, polygonManager, polylineManager, groundOverlayManager, imagesCache);
mGroundOverlayUrls = new HashSet<>();
mMarkerIconsDownloaded = false;
mGroundOverlayImagesDownloaded = false;
mUrlSanitizer = (urlSanitizer == null) ? new DefaultKmlUrlSanitizer() : urlSanitizer;
}

private static class DefaultKmlUrlSanitizer implements KmlUrlSanitizer {
@Override
public String sanitizeUrl(String url) {
try {
URL parsedUrl = new URL(url);
if (parsedUrl.getProtocol().equalsIgnoreCase("http") || parsedUrl.getProtocol().equalsIgnoreCase("https")) {
return url;
}
} catch (MalformedURLException e) {
// Return null to block invalid URLs
}
return null;
}
}

/**
Expand Down Expand Up @@ -617,10 +636,11 @@ protected void onPostExecute(Bitmap bitmap) {
* @return the bitmap of that image, scaled according to screen density.
*/
private Bitmap getBitmapFromUrl(String url) throws IOException {
URL parsedUrl = new URL(url);
if (!parsedUrl.getProtocol().equalsIgnoreCase("http") && !parsedUrl.getProtocol().equalsIgnoreCase("https")) {
throw new MalformedURLException("Unsupported scheme: " + parsedUrl.getProtocol());
String sanitizedUrl = mUrlSanitizer.sanitizeUrl(url);
if (sanitizedUrl == null) {
throw new MalformedURLException("URL blocked by sanitizer: " + url);
}
URL parsedUrl = new URL(sanitizedUrl);
return BitmapFactory.decodeStream(openConnectionCheckRedirects(parsedUrl.openConnection()));
}

Expand Down Expand Up @@ -651,11 +671,9 @@ private InputStream openConnectionCheckRedirects(URLConnection c) throws IOExcep
target = new URL(base, loc);
}
http.disconnect();
// Redirection should be allowed only for HTTP and HTTPS
// and should be limited to 5 redirections at most.
if (target == null || !(target.getProtocol().equals("http")
|| target.getProtocol().equals("https"))
|| redirects >= 5) {

// Validate redirect URL using the sanitizer
if (target == null || mUrlSanitizer.sanitizeUrl(target.toString()) == null || redirects >= 5) {
throw new SecurityException("illegal URL redirect");
}
redir = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.data.kml;

/**
* Interface for sanitizing URLs in KML documents.
* Developers can implement this to control which external resources (images, etc.) are loaded.
*/
public interface KmlUrlSanitizer {
/**
* Sanitizes a URL before it is used to fetch a resource.
*
* @param url The raw URL from the KML.
* @return A safe, validated URL string, or null to block this resource.
*/
String sanitizeUrl(String url);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void setUp() throws Exception {
mParser = new KmlParser(parser);
mParser.parseKml();

mRenderer = new KmlRenderer(mMap1, null, null, null, null, null, null);
mRenderer = new KmlRenderer(mMap1, null, null, null, null, null, null, null);
mRenderer.storeKmlData(mParser.getStyles(), mParser.getStyleMaps(), mParser.getPlacemarks(),
mParser.getContainers(), mParser.getGroundOverlays());
}
Expand All @@ -65,7 +65,7 @@ public void testAssignStyleMap() {
KmlStyle redStyle = new KmlStyle();
styles.put("BlueValue", blueStyle);
styles.put("RedValue", redStyle);
KmlRenderer renderer = new KmlRenderer(null, null, null, null, null, null, null);
KmlRenderer renderer = new KmlRenderer(null, null, null, null, null, null, null, null);
renderer.assignStyleMap(styleMap, styles);
assertNotNull(styles.get("BlueKey"));
assertEquals(styles.get("BlueKey"), styles.get("BlueValue"));
Expand All @@ -80,7 +80,7 @@ public void testAssignStyleMap() {

@Test
public void testBitmapUrlSchemeValidation() throws Exception {
KmlRenderer renderer = new KmlRenderer(null, null, null, null, null, null, null);
KmlRenderer renderer = new KmlRenderer(null, null, null, null, null, null, null, null);
java.lang.reflect.Method method = KmlRenderer.class.getDeclaredMethod("getBitmapFromUrl", String.class);
method.setAccessible(true);

Expand All @@ -90,7 +90,7 @@ public void testBitmapUrlSchemeValidation() throws Exception {
org.junit.Assert.fail("Should have thrown InvocationTargetException containing MalformedURLException");
} catch (java.lang.reflect.InvocationTargetException e) {
assertTrue(e.getCause() instanceof java.net.MalformedURLException);
assertEquals("Unsupported scheme: file", e.getCause().getMessage());
assertTrue(e.getCause().getMessage().contains("URL blocked by sanitizer"));
}

// Should throw MalformedURLException for ftp:// scheme
Expand All @@ -99,20 +99,20 @@ public void testBitmapUrlSchemeValidation() throws Exception {
org.junit.Assert.fail("Should have thrown InvocationTargetException containing MalformedURLException");
} catch (java.lang.reflect.InvocationTargetException e) {
assertTrue(e.getCause() instanceof java.net.MalformedURLException);
assertEquals("Unsupported scheme: ftp", e.getCause().getMessage());
assertTrue(e.getCause().getMessage().contains("URL blocked by sanitizer"));
}

// For http/https, it should not throw MalformedURLException with "Unsupported scheme"
// For http/https, it should not throw MalformedURLException with "URL blocked by sanitizer"
try {
method.invoke(renderer, "http://example.com/image.png");
} catch (java.lang.reflect.InvocationTargetException e) {
org.junit.Assert.assertFalse(e.getCause().getMessage() != null && e.getCause().getMessage().startsWith("Unsupported scheme"));
org.junit.Assert.assertFalse(e.getCause().getMessage() != null && e.getCause().getMessage().contains("URL blocked by sanitizer"));
}

try {
method.invoke(renderer, "https://example.com/image.png");
} catch (java.lang.reflect.InvocationTargetException e) {
org.junit.Assert.assertFalse(e.getCause().getMessage() != null && e.getCause().getMessage().startsWith("Unsupported scheme"));
org.junit.Assert.assertFalse(e.getCause().getMessage() != null && e.getCause().getMessage().contains("URL blocked by sanitizer"));
}
}
}
Loading