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 @@ -26,7 +26,13 @@ package com.google.maps.android.data.parser.geojson
* @property lng The longitude of the coordinate.
* @property alt The altitude of the coordinate, in meters. Optional.
*/
data class Coordinates(val lat: Double, val lng: Double, val alt: Double? = null)
data class Coordinates(val lat: Double, val lng: Double, val alt: Double? = null) {
init {
require(lat.isFinite() && lng.isFinite() && (alt == null || alt.isFinite())) {
"GeoJSON coordinate contains a non-finite value"
}
}
}

// Using a sealed interface for all GeoJSON objects
sealed interface GeoJsonObject {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ data class Metadata(
val time: String? = null,
)

/**
* Represents a GPX waypoint (`<wpt>`), point of interest, or named feature on a map.
*
* @property lat The latitude of the waypoint.
* @property lon The longitude of the waypoint.
* @property ele The elevation (in meters) of the waypoint, or null if unspecified.
* @property time The timestamp of the waypoint.
* @property name The name of the waypoint.
* @property desc A description of the waypoint.
* @property sym The symbol name or icon for the waypoint.
*/
@Serializable
@XmlSerialName("wpt", namespace = GPX_NAMESPACE, prefix = "")
data class Wpt(
Expand All @@ -78,7 +89,19 @@ data class Wpt(
@XmlElement(true)
@XmlSerialName("sym", namespace = GPX_NAMESPACE, prefix = "")
val sym: String? = null,
)
) {
/**
* Descriptive alias for [ele] (elevation in meters).
*/
val elevation: Double?
get() = ele

init {
require(lat.isFinite() && lon.isFinite() && (elevation?.isFinite() ?: true)) {
"GPX coordinate contains a non-finite value"
}
}
}

@Serializable
@XmlSerialName("rte", namespace = GPX_NAMESPACE, prefix = "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ class GpxParser {
XML {
defaultPolicy {
ignoreUnknownChildren()
isCollectingNSAttributes = true
}
isCollectingNSAttributes = true
}

fun parse(inputStream: InputStream): Gpx {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class KmlParser {
XML {
defaultPolicy {
ignoreUnknownChildren()
isCollectingNSAttributes = true
}
isCollectingNSAttributes = true
}

fun parseAsKml(inputStream: InputStream): Kml {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,10 @@ data class LatLngAlt(
val latitude: Double,
val longitude: Double,
val altitude: Double? = null,
)
) {
init {
require(latitude.isFinite() && longitude.isFinite() && (altitude == null || altitude.isFinite())) {
"KML coordinate contains a non-finite value"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,16 @@ internal object LatLngAltSerializer : KSerializer<LatLngAlt> {

internal fun parse(string: String): LatLngAlt {
val parts = string.split(",").map { it.trim().toDouble() }
val lng = parts[0]
val lat = parts[1]
val alt = parts.getOrNull(2)
require(lng.isFinite() && lat.isFinite() && (alt == null || alt.isFinite())) {
"KML coordinate contains a non-finite value"
}
return LatLngAlt(
longitude = parts[0],
latitude = parts[1],
altitude = parts.getOrNull(2),
longitude = lng,
latitude = lat,
altitude = alt,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ object GeoJsonMapper {
geometry is LineString || (geometry is MultiGeometry && !geometry.isPolygonal()) -> {
// MultiGeometry could contain lines
val strokeColor = props["stroke"]?.let { parseColor(it) }
val strokeWidth = props["stroke-width"]?.toFloatOrNull()
val strokeWidth = props["stroke-width"]?.toFloatOrNull()?.takeIf { it.isFinite() && it >= 0f }
if (strokeColor != null || strokeWidth != null) {
LineStyle(
color = strokeColor ?: 0xFF000000.toInt(),
Expand All @@ -105,10 +105,10 @@ object GeoJsonMapper {
}
geometry is ModelPolygon || (geometry is MultiGeometry && geometry.isPolygonal()) -> {
val strokeColor = props["stroke"]?.let { parseColor(it) }
val strokeWidth = props["stroke-width"]?.toFloatOrNull()
val strokeWidth = props["stroke-width"]?.toFloatOrNull()?.takeIf { it.isFinite() && it >= 0f }
val fillColor = props["fill"]?.let { parseColor(it) }
val fillOpacity = props["fill-opacity"]?.toFloatOrNull()
val strokeOpacity = props["stroke-opacity"]?.toFloatOrNull()
val fillOpacity = props["fill-opacity"]?.toFloatOrNull()?.takeIf { it.isFinite() && it in 0f..1f }
val strokeOpacity = props["stroke-opacity"]?.toFloatOrNull()?.takeIf { it.isFinite() && it in 0f..1f }

val finalFillColor = if (fillColor != null && fillOpacity != null) {
applyOpacity(fillColor, fillOpacity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ private fun KmlStyle.toRendererStyle(geometry: Geometry): Style? =
is PointGeometry -> {
iconStyle?.let {
PointStyle(
scale = it.scale,
scale = it.scale.takeIf { s -> s.isFinite() && s >= 0f } ?: 1.0f,
iconUrl = it.icon?.href,
// TODO: Map other properties like heading, hotSpot if needed
)
Expand All @@ -210,7 +210,7 @@ private fun KmlStyle.toRendererStyle(geometry: Geometry): Style? =
lineStyle?.let {
LineStyle(
color = convertKmlColor(it.color ?: 0xFF000000.toInt()),
width = it.width ?: 1.0f,
width = it.width?.takeIf { w -> w.isFinite() && w >= 0f } ?: 1.0f,
)
}
}
Expand All @@ -220,7 +220,7 @@ private fun KmlStyle.toRendererStyle(geometry: Geometry): Style? =
PolygonStyle(
fillColor = if (it.fill) convertKmlColor(it.color ?: 0x00000000) else 0x00000000,
strokeColor = convertKmlColor(lineStyle?.color ?: 0xFF000000.toInt()),
strokeWidth = lineStyle?.width ?: 1.0f,
strokeWidth = lineStyle?.width?.takeIf { w -> w.isFinite() && w >= 0f } ?: 1.0f,
// TODO: Handle outline property
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* 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.parser

import com.google.common.truth.Truth.assertThat
import com.google.maps.android.data.parser.geojson.GeoJsonParser
import com.google.maps.android.data.parser.gpx.GpxParser
import com.google.maps.android.data.parser.kml.KmlParser
import com.google.maps.android.data.renderer.mapper.GeoJsonMapper
import com.google.maps.android.data.renderer.mapper.toLayer
import com.google.maps.android.data.renderer.model.LineStyle
import com.google.maps.android.data.renderer.model.PolygonStyle
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import kotlin.test.assertFailsWith

@RunWith(RobolectricTestRunner::class)
class SecurityHardeningTest {
private val kmlParser = KmlParser()
private val gpxParser = GpxParser()
private val geoJsonParser = GeoJsonParser()

@Test
fun testKmlCoordinateNanPoisoning_throwsException() {
val kml = """
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<Point>
<coordinates>NaN,NaN,0</coordinates>
</Point>
</Placemark>
</Document>
</kml>
""".trimIndent()

assertFailsWith<Exception> {
val parsed = kmlParser.parseAsKml(kml.byteInputStream())
parsed.toLayer()
}
}

@Test
fun testKmlCoordinateInfinityPoisoning_throwsException() {
val kml = """
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<Point>
<coordinates>10.0,Infinity,0</coordinates>
</Point>
</Placemark>
</Document>
</kml>
""".trimIndent()

assertFailsWith<Exception> {
val parsed = kmlParser.parseAsKml(kml.byteInputStream())
parsed.toLayer()
}
}

@Test
fun testGpxCoordinateNanPoisoning_throwsException() {
val gpx = """
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1">
<wpt lat="NaN" lon="10.0">
<name>Poisoned Waypoint</name>
</wpt>
</gpx>
""".trimIndent()

assertFailsWith<Exception> {
val parsed = gpxParser.parse(gpx.byteInputStream())
parsed.toLayer()
}
}

@Test
fun testGpxCoordinateInfinityPoisoning_throwsException() {
val gpx = """
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1">
<wpt lat="10.0" lon="-Infinity">
<name>Poisoned Waypoint</name>
</wpt>
</gpx>
""".trimIndent()

assertFailsWith<Exception> {
val parsed = gpxParser.parse(gpx.byteInputStream())
parsed.toLayer()
}
}

@Test
fun testGeoJsonNonFiniteStyleProperties_sanitizedToSafeDefaults() {
val json = """
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0], [0.0, 0.0]]]
},
"properties": {
"stroke-width": "NaN",
"fill-opacity": "Infinity",
"stroke-opacity": "-5.0",
"stroke": "#FF0000",
"fill": "#00FF00"
}
}
]
}
""".trimIndent()

val layer = geoJsonParser.parse(json.byteInputStream())!!.toLayer()
val feature = layer.features.first()
val style = feature.style as PolygonStyle

// Non-finite width must fall back to safe default 1.0f rather than Float.NaN
assertThat(style.strokeWidth).isEqualTo(1.0f)
assertThat(style.strokeWidth.isFinite()).isTrue()
}

@Test
fun testGeoJsonLineStringNonFiniteStrokeWidth_sanitizedToSafeDefault() {
val json = """
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[0.0, 0.0], [10.0, 10.0]]
},
"properties": {
"stroke-width": "Infinity",
"stroke": "#0000FF"
}
}
]
}
""".trimIndent()

val layer = geoJsonParser.parse(json.byteInputStream())!!.toLayer()
val feature = layer.features.first()
val style = feature.style as LineStyle

// Infinity width must fall back to safe default 1.0f rather than Float.POSITIVE_INFINITY
assertThat(style.width).isEqualTo(1.0f)
assertThat(style.width.isFinite()).isTrue()
}

@Test
fun testKmlNonFiniteStyleProperties_sanitizedToSafeDefaults() {
val kml = """
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Style id="poisonedStyle">
<LineStyle>
<width>NaN</width>
</LineStyle>
<IconStyle>
<scale>Infinity</scale>
</IconStyle>
</Style>
<Placemark>
<styleUrl>#poisonedStyle</styleUrl>
<LineString>
<coordinates>0,0,0 10,10,0</coordinates>
</LineString>
</Placemark>
</Document>
</kml>
""".trimIndent()

// Should parse safely without crashing and sanitize non-finite width/scale to safe defaults
val layer = kmlParser.parseAsKml(kml.byteInputStream()).toLayer()
val feature = layer.features.first()
val style = feature.style as LineStyle
assertThat(style.width.isFinite()).isTrue()
assertThat(style.width).isAtLeast(0.0f)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,11 @@ class GpxParserTest {
assertTrue(trkFeature.geometry is LineString)
assertEquals("Trk1", trkFeature.properties["name"])
}

@Test
fun `test Wpt elevation property alias`() {
val wpt = Wpt(lat = 1.0, lon = 2.0, ele = 123.45)
assertEquals(123.45, wpt.ele)
assertEquals(123.45, wpt.elevation)
}
}
Loading