Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -78,7 +78,13 @@ data class Wpt(
@XmlElement(true)
@XmlSerialName("sym", namespace = GPX_NAMESPACE, prefix = "")
val sym: String? = null,
)
) {
init {
require(lat.isFinite() && lon.isFinite() && (ele == null || ele.isFinite())) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to think for a millisecond what was "ele", not sure if it could be worth adding the entire name.

"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 @@ -17,6 +17,8 @@ package com.google.maps.android.data.parser.kml

import com.google.common.truth.Truth.assertThat
import com.google.maps.android.data.parser.kml.assertThat
import nl.adaptivity.xmlutil.XmlException
import nl.adaptivity.xmlutil.serialization.XmlParsingException
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
Expand Down Expand Up @@ -658,7 +660,7 @@ class KmlParserTest {
@Test
fun testAmuWrongNotExistLatitudeCoordinates() {
val stream = File("src/test/resources/amu_wrong_not_exist_latitude_coordinates.kml").inputStream()
assertFailsWith<nl.adaptivity.xmlutil.serialization.XmlParsingException> {
assertFailsWith<XmlException> {
parser.parseAsKml(stream)
}
}
Expand All @@ -672,7 +674,7 @@ class KmlParserTest {
append("</Document></kml>")
}

assertFailsWith<nl.adaptivity.xmlutil.serialization.XmlParsingException> {
assertFailsWith<XmlParsingException> {
parser.parseAsKml(kml.byteInputStream())
}
}
Expand All @@ -687,7 +689,7 @@ class KmlParserTest {
append("</Placemark></Document></kml>")
}

assertFailsWith<nl.adaptivity.xmlutil.serialization.XmlParsingException> {
assertFailsWith<XmlParsingException> {
parser.parseAsKml(kml.byteInputStream())
}
}
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ dokka-gradle-plugin = "2.2.0"
gradle = "9.3.1"
gradleMavenPublishPlugin = "0.37.0"
secrets-gradle-plugin = "2.0.1"
serialization = "0.91.3"
serialization = "1.0.1"


[libraries]
Expand Down