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.
This commit is contained in:
b0txec
2026-08-24 11:55:52 +03:00
parent 6b9c7cf4ae
commit 8c45d8de1d
2 changed files with 10 additions and 2 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ import fetch.csv.FileNameService
import fetch.lvgmc.{FetchService, WaterTemperatureService} import fetch.lvgmc.{FetchService, WaterTemperatureService}
import fetch.warnings.WarningService import fetch.warnings.WarningService
import fs2.io.file.{Files, Path} 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._
import org.http4s.dsl.io._ import org.http4s.dsl.io._
import org.http4s.implicits._ 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 // 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) val userQuery = UserQuery(cities, field, key, granularity, from, to)
postgresService.query(userQuery) postgresService.query(userQuery)
@@ -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 { object AggFieldList {
def unapply(str: String): Option[NonEmptyList[String]] = { def unapply(str: String): Option[NonEmptyList[String]] = {
val weatherFields = WeatherData.getKeys val weatherFields = WeatherData.getKeys