Skip to content

Commit

Permalink
fix #1971
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieuancelin committed Sep 12, 2024
1 parent 7b58d41 commit c730961
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 2 deletions.
4 changes: 4 additions & 0 deletions otoroshi/app/env/Env.scala
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ class Env(
private lazy val halloweenStop =
DateTime.now().withMonthOfYear(10).withDayOfMonth(31).plusDays(1).withMillisOfDay(1)

lazy val maxHeaderSizeToBackend =
configuration.getOptionalWithFileSupport[Long]("otoroshi.options.maxHeaderSizeToBackend")
lazy val maxHeaderSizeToClient =
configuration.getOptionalWithFileSupport[Long]("otoroshi.options.maxHeaderSizeToClient")
lazy val jsonPathNullReadIsJsNull =
configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.jsonPathNullReadIsJsNull").getOrElse(false)

Expand Down
119 changes: 119 additions & 0 deletions otoroshi/app/next/plugins/headers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import otoroshi.models.RemainingQuotas
import otoroshi.next.plugins.api._
import otoroshi.utils.http.RequestImplicits.EnhancedRequestHeader
import otoroshi.utils.syntax.implicits._
import play.api.Logger
import play.api.libs.json._
import play.api.mvc.{Result, Results}

Expand Down Expand Up @@ -685,3 +686,121 @@ class ForwardedHeader extends NgRequestTransformer {
Right(ctx.otoroshiRequest.copy(headers = ctx.otoroshiRequest.headers ++ additionalHeaders.toMap))
}
}

case class RejectHeaderConfig(value: Long = 8 * 1024) extends NgPluginConfig {
def json: JsValue = RejectHeaderConfig.format.writes(this)
}

object RejectHeaderConfig {
val default = RejectHeaderConfig()
val configFlow: Seq[String] = Seq("value")
val configSchema: Option[JsObject] = Some(Json.obj(
"value" -> Json.obj(
"type" -> "number",
"label" -> "Max length",
"props" -> Json.obj(
"label" -> "Max Length",
"suffix" -> "bytes"
)
)
))
val format = new Format[RejectHeaderConfig] {

override def reads(json: JsValue): JsResult[RejectHeaderConfig] = Try {
RejectHeaderConfig(
value = json.select("value").asOpt[Long].getOrElse(8 * 1024)
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(e) => JsSuccess(e)
}

override def writes(o: RejectHeaderConfig): JsValue = Json.obj(
"value" -> o.value
)
}
}

class RejectHeaderInTooLong extends NgRequestTransformer {

private val logger = Logger("otoroshi-plugin-reject-headers-in-too-long")
override def steps: Seq[NgStep] = Seq(NgStep.TransformRequest)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Headers)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand

override def multiInstance: Boolean = true
override def core: Boolean = true
override def usesCallbacks: Boolean = false
override def transformsRequest: Boolean = true
override def transformsResponse: Boolean = false
override def transformsError: Boolean = false
override def isTransformRequestAsync: Boolean = false
override def isTransformResponseAsync: Boolean = true
override def name: String = "Reject headers in too long"
override def description: Option[String] =
"This plugin remove all headers to backend with a length above a max".some
override def defaultConfigObject: Option[NgPluginConfig] = Some(RejectHeaderConfig())

override def noJsForm: Boolean = true

override def configFlow: Seq[String] = RejectHeaderConfig.configFlow

override def configSchema: Option[JsObject] = RejectHeaderConfig.configSchema

override def transformRequestSync(
ctx: NgTransformerRequestContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Either[Result, NgPluginHttpRequest] = {
val config = ctx.cachedConfig(internalName)(RejectHeaderConfig.format).getOrElse(RejectHeaderConfig())
Right(ctx.otoroshiRequest.copy(
headers = ctx.otoroshiRequest.headers.filter {
case (key, value) if key.length > config.value => {
logger.error(s"removing header '${key}' from request to backend because it's too long. route is ${ctx.route.name} / ${ctx.route.id}. header value length is '${value.length}' and value is '${value}'")
false
}
case _ => true
}
))
}
}

class RejectHeaderOutTooLong extends NgRequestTransformer {

private val logger = Logger("otoroshi-plugin-reject-headers-out-too-long")
override def steps: Seq[NgStep] = Seq(NgStep.TransformRequest)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Headers)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand

override def multiInstance: Boolean = true
override def core: Boolean = true
override def usesCallbacks: Boolean = false
override def transformsRequest: Boolean = false
override def transformsResponse: Boolean = true
override def transformsError: Boolean = false
override def isTransformRequestAsync: Boolean = false
override def isTransformResponseAsync: Boolean = false
override def name: String = "Reject headers out too long"
override def description: Option[String] =
"This plugin remove all headers from backend with a length above a max".some
override def defaultConfigObject: Option[NgPluginConfig] = Some(RejectHeaderConfig())

override def noJsForm: Boolean = true

override def configFlow: Seq[String] = RejectHeaderConfig.configFlow

override def configSchema: Option[JsObject] = RejectHeaderConfig.configSchema

override def transformResponseSync(
ctx: NgTransformerResponseContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Either[Result, NgPluginHttpResponse] = {
val config = ctx.cachedConfig(internalName)(RejectHeaderConfig.format).getOrElse(RejectHeaderConfig())
Right(ctx.otoroshiResponse.copy(
headers = ctx.otoroshiResponse.headers.filter {
case (key, value) if key.length > config.value => {
logger.error(s"removing header '${key}' from response to client because it's too long. route is ${ctx.route.name} / ${ctx.route.id}. header value length is '${value.length}' and value is '${value}'")
false
}
case _ => true
}
))
}
}
26 changes: 24 additions & 2 deletions otoroshi/app/next/proxy/engine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2768,7 +2768,7 @@ class ProxyEngine() extends RequestHandler {
request.headers.toSeq,
route.serviceDescriptor,
target = finalTarget,
rawRequest = rawRequest,
rawRequest = rawRequest, // TODO: custom header size
route = route.some
)
.right
Expand All @@ -2782,7 +2782,7 @@ class ProxyEngine() extends RequestHandler {
UrlSanitizer.sanitize(request.url),
out,
request.headers.toSeq,
rawRequest,
rawRequest, // TODO: custom header size
route.serviceDescriptor,
route.some,
ctxPlugins.some,
Expand Down Expand Up @@ -2908,6 +2908,17 @@ class ProxyEngine() extends RequestHandler {
.applyOnIf(isTargetHttp2 && isRequestAboveHttp2) { s =>
s.filterNot(_._1.toLowerCase().startsWith("x-http3"))
}
.applyOnWithOpt(env.maxHeaderSizeToBackend) {
case (hdrs, max) => {
hdrs.filter {
case (key, value) if key.length > max => {
logger.error(s"removing header '${key}' from request to backend because it's too long. route is ${route.name} / ${route.id}. header value length is '${value.length}' and value is '${value}'")
false
}
case _ => true
}
}
}
val builder = clientReq
.withRequestTimeout(extractedTimeout)
.withFailureIndicator(fakeFailureIndicator)
Expand Down Expand Up @@ -3328,6 +3339,17 @@ class ProxyEngine() extends RequestHandler {
headersOutFiltered.contains(key.toLowerCase())
}
.applyOnIf(!isHttp10)(_.filterNot(h => h._1.toLowerCase() == "content-length"))
.applyOnWithOpt(env.maxHeaderSizeToClient) {
case (hdrs, max) => {
hdrs.filter {
case (key, value) if key.length > max => {
logger.error(s"removing header '${key}' from response because it's too long. route is ${route.name} / ${route.id}. header value length is '${value.length}' and value is '${value}'")
false
}
case _ => true
}
}
}
.toSeq // ++ Seq(("Connection" -> "keep-alive"), ("X-Connection" -> "keep-alive"))

val theBody = response.body
Expand Down
4 changes: 4 additions & 0 deletions otoroshi/conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,10 @@ otoroshi {
dynamicBodySizeCompute = ${?OTOROSHI_OPTIONS_DYNAMIC_BODY_SIZE_COMPUTE}
allowRedirectQueryParamOnLogin = false
allowRedirectQueryParamOnLogin = ${?OTOROSHI_OPTIONS_ALLOW_REDIRECT_QUERY_PARAM_ON_LOGIN}
maxHeaderSizeToBackend = 5
maxHeaderSizeToBackend = ${?OTOROSHI_OPTIONS_MAX_HEADER_SIZE_TO_BACKEND}
maxHeaderSizeToClient = 3
maxHeaderSizeToClient = ${?OTOROSHI_OPTIONS_MAX_HEADER_SIZE_TO_CLIENT}
}
wasm {
cache {
Expand Down

0 comments on commit c730961

Please sign in to comment.