diff --git a/data/src/main/java/com/google/maps/android/data/kml/DefaultKmlUrlSanitizer.java b/data/src/main/java/com/google/maps/android/data/kml/DefaultKmlUrlSanitizer.java new file mode 100644 index 000000000..603023318 --- /dev/null +++ b/data/src/main/java/com/google/maps/android/data/kml/DefaultKmlUrlSanitizer.java @@ -0,0 +1,81 @@ +/* + * 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; + +import java.net.InetAddress; +import java.net.URI; +import java.util.Locale; + +/** + * Default, secure {@link KmlUrlSanitizer} used when a layer loads remote icons/overlays. + * + *
Icon and GroundOverlay {@code This sanitizer allows only {@code http}/{@code https} URLs whose host does not resolve to a
+ * loopback, any-local, link-local, site-local (RFC 1918), or multicast address, and returns
+ * {@code null} (block) otherwise. Public icon/overlay URLs continue to load unchanged.
+ */
+public class DefaultKmlUrlSanitizer implements KmlUrlSanitizer {
+
+ @Override
+ public String sanitizeUrl(String url) {
+ if (url == null) {
+ return null;
+ }
+ final URI uri;
+ try {
+ uri = new URI(url);
+ } catch (Exception e) {
+ return null;
+ }
+
+ final String scheme = uri.getScheme();
+ if (scheme == null) {
+ return null;
+ }
+ final String lowerScheme = scheme.toLowerCase(Locale.ROOT);
+ if (!lowerScheme.equals("http") && !lowerScheme.equals("https")) {
+ return null;
+ }
+
+ final String host = uri.getHost();
+ if (host == null || host.isEmpty()) {
+ return null;
+ }
+
+ try {
+ // Block if ANY resolved address is internal, to defeat split-horizon / multi-A tricks.
+ for (InetAddress address : InetAddress.getAllByName(host)) {
+ if (address.isLoopbackAddress()
+ || address.isAnyLocalAddress()
+ || address.isLinkLocalAddress()
+ || address.isSiteLocalAddress()
+ || address.isMulticastAddress()) {
+ return null;
+ }
+ }
+ } catch (Exception e) {
+ // Unresolvable host: fail closed.
+ return null;
+ }
+
+ return url;
+ }
+}
diff --git a/data/src/main/java/com/google/maps/android/data/renderer/UrlIconProvider.kt b/data/src/main/java/com/google/maps/android/data/renderer/UrlIconProvider.kt
index b2ffb3dea..e66f44209 100644
--- a/data/src/main/java/com/google/maps/android/data/renderer/UrlIconProvider.kt
+++ b/data/src/main/java/com/google/maps/android/data/renderer/UrlIconProvider.kt
@@ -18,6 +18,8 @@ package com.google.maps.android.data.renderer
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.LruCache
+import com.google.maps.android.data.kml.DefaultKmlUrlSanitizer
+import com.google.maps.android.data.kml.KmlUrlSanitizer
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
@@ -35,6 +37,11 @@ import java.util.concurrent.ConcurrentHashMap
*/
open class UrlIconProvider(
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ // Icon/overlay hrefs originate from the (frequently untrusted) KML/GeoJSON document. The
+ // sanitizer is applied before every fetch to prevent SSRF; the secure default blocks
+ // non-http(s) schemes and hosts resolving to loopback/link-local/private ranges. Pass a
+ // custom implementation to widen/narrow the policy, or `null` to disable (not recommended).
+ private val urlSanitizer: KmlUrlSanitizer? = DefaultKmlUrlSanitizer(),
) : IconProvider {
private val memoryCache: LruCache