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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user