From c5bbf0d6e1821c36a6078376384bc2cab113ee35 Mon Sep 17 00:00:00 2001 From: spenes Date: Wed, 11 Sep 2024 14:36:54 +0300 Subject: [PATCH] Add max JSON depth check to /keygen endpoint --- .../iglu/server/Server.scala | 2 +- .../snowplowanalytics/iglu/server/Utils.scala | 17 ++++++++- .../iglu/server/service/AuthService.scala | 16 +++++--- .../iglu/server/service/SchemaService.scala | 14 +------ .../iglu/server/service/AuthServiceSpec.scala | 37 ++++++++++++++++++- 5 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/Server.scala b/src/main/scala/com/snowplowanalytics/iglu/server/Server.scala index 137931a..1a7db02 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/Server.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/Server.scala @@ -147,7 +147,7 @@ object Server { val services: List[(String, RoutesConstructor)] = List( "/api/meta" -> MetaService.asRoutes(debug, patchesAllowed, isHealthy), "/api/schemas" -> SchemaService.asRoutes(patchesAllowed, webhook, maxJsonDepth), - "/api/auth" -> AuthService.asRoutes, + "/api/auth" -> AuthService.asRoutes(maxJsonDepth), "/api/validation" -> ValidationService.asRoutes(maxJsonDepth), "/api/drafts" -> DraftService.asRoutes(maxJsonDepth) ) diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/Utils.scala b/src/main/scala/com/snowplowanalytics/iglu/server/Utils.scala index b930411..1fa1f8f 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/Utils.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/Utils.scala @@ -10,7 +10,7 @@ package com.snowplowanalytics.iglu.server -import io.circe.{Encoder, Json} +import io.circe.{Decoder, Encoder, Json} import io.circe.syntax._ import cats.implicits._ @@ -18,7 +18,7 @@ import cats.effect.Sync import fs2.{Stream, text} -import org.http4s.{EntityDecoder, InvalidMessageBodyFailure} +import org.http4s.{DecodeResult, EntityDecoder, InvalidMessageBodyFailure} import org.http4s.circe.jsonDecoder import com.snowplowanalytics.iglu.core.{SchemaKey, SchemaMap, SchemaVer} @@ -31,6 +31,19 @@ object Utils { def toBytes[F[_], A: Encoder](a: A): Stream[F, Byte] = Stream.emit(a.asJson.noSpaces).through(text.utf8Encode) + def jsonOfWithDepthCheck[F[_]: Sync, A: Decoder](maxJsonDepth: Int): EntityDecoder[F, A] = + jsonDecoderWithDepthCheck(maxJsonDepth).flatMapR { json => + json + .as[A] + .fold( + failure => + DecodeResult.failureT[F, A]( + InvalidMessageBodyFailure(s"Could not decode JSON body", Some(failure)) + ), + DecodeResult.successT[F, A](_) + ) + } + def jsonDecoderWithDepthCheck[F[_]: Sync](maxJsonDepth: Int): EntityDecoder[F, Json] = jsonDecoder[F].transform( _.flatMap { json => diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/service/AuthService.scala b/src/main/scala/com/snowplowanalytics/iglu/server/service/AuthService.scala index 273f5e2..5ab7233 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/service/AuthService.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/service/AuthService.scala @@ -33,18 +33,24 @@ import com.snowplowanalytics.iglu.server.model.Permission import com.snowplowanalytics.iglu.server.model.IgluResponse import com.snowplowanalytics.iglu.server.storage.Storage -class AuthService[F[+_]: Sync](swagger: SwaggerSyntax[F], ctx: AuthedContext[F, Permission], db: Storage[F]) - extends RhoRoutes[F] { +class AuthService[F[+_]: Sync]( + swagger: SwaggerSyntax[F], + ctx: AuthedContext[F, Permission], + db: Storage[F], + maxJsonDepth: Int +) extends RhoRoutes[F] { import swagger._ import AuthService._ val apikey = paramD[UUID]("key", "UUID apikey to delete") + val jsonGenerateKey = Utils.jsonOfWithDepthCheck[F, GenerateKey](maxJsonDepth) + "Route to delete api key" ** DELETE / "keygen" +? apikey >>> ctx.auth |>> deleteKey _ "Route to generate new keys" ** - POST / "keygen" >>> ctx.auth ^ jsonOf[F, GenerateKey] |>> { (authInfo: Permission, gk: GenerateKey) => + POST / "keygen" >>> ctx.auth ^ jsonGenerateKey |>> { (authInfo: Permission, gk: GenerateKey) => if (authInfo.key.contains(Permission.KeyAction.Create)) { val vendorPrefix = Permission.Vendor.parse(gk.vendorPrefix) if (authInfo.canCreatePermission(vendorPrefix.asString)) { @@ -71,13 +77,13 @@ object AuthService { implicit val schemaGenerateReq: Decoder[GenerateKey] = deriveDecoder[GenerateKey] - def asRoutes( + def asRoutes(maxJsonDepth: Int)( db: Storage[IO], superKey: Option[UUID], ctx: AuthedContext[IO, Permission], rhoMiddleware: RhoMiddleware[IO] ): HttpRoutes[IO] = { - val service = new AuthService(swaggerSyntax, ctx, db).toRoutes(rhoMiddleware) + val service = new AuthService(swaggerSyntax, ctx, db, maxJsonDepth).toRoutes(rhoMiddleware) PermissionMiddleware.wrapService(db, superKey, ctx, service) } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/service/SchemaService.scala b/src/main/scala/com/snowplowanalytics/iglu/server/service/SchemaService.scala index fa05937..8859f47 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/service/SchemaService.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/service/SchemaService.scala @@ -19,7 +19,7 @@ import cats.implicits._ import io.circe.Json -import org.http4s.{DecodeResult, HttpRoutes, InvalidMessageBodyFailure} +import org.http4s.HttpRoutes import org.http4s.rho.{AuthedContext, RhoMiddleware, RhoRoutes} import org.http4s.rho.swagger.SwaggerSyntax import org.http4s.rho.swagger.syntax.{io => swaggerSyntax} @@ -56,17 +56,7 @@ class SchemaService[F[+_]: Sync]( val version = pathVar[SchemaVer.Full]("version", "SchemaVer") val isPublic = paramD[Boolean]("isPublic", false, "Should schema be created as public") - val schemaOrJson = Utils.jsonDecoderWithDepthCheck(maxJsonDepth).flatMapR { json => - json - .as[SchemaBody] - .fold( - failure => - DecodeResult.failureT[F, SchemaBody]( - InvalidMessageBodyFailure(s"Could not decode JSON: ${json.noSpaces}", Some(failure)) - ), - DecodeResult.successT[F, SchemaBody](_) - ) - } + val schemaOrJson = Utils.jsonOfWithDepthCheck[F, SchemaBody](maxJsonDepth) val jsonBody = Utils.jsonDecoderWithDepthCheck(maxJsonDepth) diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/service/AuthServiceSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/service/AuthServiceSpec.scala index 4ffc2cc..6da91b5 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/service/AuthServiceSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/service/AuthServiceSpec.scala @@ -18,11 +18,16 @@ import org.http4s.rho.swagger.syntax.io.createRhoMiddleware import java.util.UUID import com.snowplowanalytics.iglu.server.storage.InMemory +import com.snowplowanalytics.iglu.server.SpecHelpers._ class AuthServiceSpec extends org.specs2.Specification with StorageAgnosticSpec with InMemoryStorageSpec { - def getState(request: Request[IO], superApiKey: Option[UUID] = None): IO[(List[Response[IO]], InMemory.State)] = + def getState( + request: Request[IO], + superApiKey: Option[UUID] = None, + maxJsonDepth: Int = 20 + ): IO[(List[Response[IO]], InMemory.State)] = sendRequestsGetState[InMemory.State](storage => - AuthService.asRoutes(storage, superApiKey, SpecHelpers.ctx, createRhoMiddleware()) + AuthService.asRoutes(maxJsonDepth)(storage, superApiKey, SpecHelpers.ctx, createRhoMiddleware()) )(storage => storage.asInstanceOf[InMemory[IO]].ref.get)(List(request)) def is = s2""" @@ -32,6 +37,7 @@ class AuthServiceSpec extends org.specs2.Specification with StorageAgnosticSpec /keygen doesn't authorize without apikey header $e4 /keygen doesn't authorize with unkown apikey in header $e5 /keygen deletes key $e6 + /keygen rejects JSON body that exceeds maximum allowed JSON depth $e7 """ def e1 = { @@ -150,4 +156,31 @@ class AuthServiceSpec extends org.specs2.Specification with StorageAgnosticSpec nokey.and(deletedResponse) } + + def e7 = { + val deepJsonSchema = createDeepJsonSchema(100000) + val deepJsonArray = createDeepJsonArray(1000000) + val wrongApikey = "c99ce0f9-cb5b-4b6f-88f3-2baed041be9b" + + def executeTest(body: String, apikey: String) = { + val req = Request( + Method.POST, + Uri.uri("/keygen"), + headers = Headers.of(Header("apikey", apikey)), + body = toBytes(body) + ) + + val expected = List((422, "The request body was invalid.")) + + val (resp, _) = getState(req).unsafeRunSync() + val result = resp.map(r => (r.status.code, r.bodyText.compile.foldMonoid.unsafeRunSync())) + + result must beEqualTo(expected) + } + + executeTest(deepJsonSchema, SpecHelpers.superKey.toString) + executeTest(deepJsonArray, wrongApikey) + executeTest(deepJsonSchema, SpecHelpers.superKey.toString) + executeTest(deepJsonArray, wrongApikey) + } }