From 8c45d8de1d2feb72a82348ddfe3e3eaad9c01dbc Mon Sep 17 00:00:00 2001 From: b0txec Date: Mon, 24 Aug 2026 11:55:52 +0300 Subject: [PATCH] Fix SQL injection in the /query/city aggregation route field on that route reached PostgresService.query unvalidated, which splices it into SQL via Fragment.const (unescaped) whenever the aggregate key is min/max/avg/sum/distinct, or whenever granularity is "hour" in the list branch. Add ValidateField (allowlists WeatherData's known field names, same pattern AggFieldList already uses for /query/country) and apply it to the field path segment. AggregateKey values reaching Fragment.const elsewhere are already safe since they come from a closed ADT, not raw user input. --- src/main/scala/server/Server.scala | 4 ++-- src/main/scala/server/ValidateRoutes.scala | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/scala/server/Server.scala b/src/main/scala/server/Server.scala index a253c87..bf88b9c 100644 --- a/src/main/scala/server/Server.scala +++ b/src/main/scala/server/Server.scala @@ -10,7 +10,7 @@ import fetch.csv.FileNameService import fetch.lvgmc.{FetchService, WaterTemperatureService} import fetch.warnings.WarningService import fs2.io.file.{Files, Path} -import server.ValidateRoutes.{AggFieldList, AggKey, CityList, DateTimeRange, Granularity, ValidateDate, ValidateDateTime, ValidateFileName, ValidateInt, ValidateMonths, ValidateZonedDateTime} +import server.ValidateRoutes.{AggFieldList, AggKey, CityList, DateTimeRange, Granularity, ValidateDate, ValidateDateTime, ValidateField, ValidateFileName, ValidateInt, ValidateMonths, ValidateZonedDateTime} import org.http4s._ import org.http4s.dsl.io._ import org.http4s.implicits._ @@ -139,7 +139,7 @@ class Server(postgresService: PostgresService, dataService: DataService, fetch: } // http://0.0.0.0:8080/api/query/city/Liepāja,Rēzekne/20230414_2200-20230501_1230/hour/tempMax/max - case GET -> Root / "query" / "city" / CityList(cities) / DateTimeRange(from, to) / Granularity(granularity) / field / AggKey(key) => + case GET -> Root / "query" / "city" / CityList(cities) / DateTimeRange(from, to) / Granularity(granularity) / ValidateField(field) / AggKey(key) => val userQuery = UserQuery(cities, field, key, granularity, from, to) postgresService.query(userQuery) diff --git a/src/main/scala/server/ValidateRoutes.scala b/src/main/scala/server/ValidateRoutes.scala index a8c87c9..94efab3 100644 --- a/src/main/scala/server/ValidateRoutes.scala +++ b/src/main/scala/server/ValidateRoutes.scala @@ -64,6 +64,14 @@ object ValidateRoutes { } } + // Restricts a raw path segment to a known WeatherData field name before it + // can reach Fragment.const (unescaped SQL splicing) in PostgresService. + object ValidateField { + def unapply(str: String): Option[String] = { + if (WeatherData.getKeys.contains(str)) Some(str) else None + } + } + object AggFieldList { def unapply(str: String): Option[NonEmptyList[String]] = { val weatherFields = WeatherData.getKeys