Skip to content

Commit

Permalink
Add a HttpModel#safeToString method which can be invoked by potenti…
Browse files Browse the repository at this point in the history
…ally PII/Sensitive information leaking code, such `HttpMessage#toString`.

It defaults to not showing more than the header name except for modeled headers which are considered safe by default and, therefore, use `toString` implementation.
10 modeled headers have been identified as containers of sensitive information and these override modeled headers default behaviour.
  • Loading branch information
pfcoperez committed Oct 2, 2019
1 parent 324f7fb commit dfb6474
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 9 deletions.
Expand Up @@ -26,6 +26,7 @@ abstract class HttpHeader extends jm.HttpHeader with ToStringRenderable {
def lowercaseName: String
def is(nameInLowerCase: String): Boolean = lowercaseName == nameInLowerCase
def isNot(nameInLowerCase: String): Boolean = lowercaseName != nameInLowerCase
def safeToString: String = name
}

object HttpHeader {
Expand Down
Expand Up @@ -362,7 +362,7 @@ final class HttpRequest(
case _ => false
}

override def toString = s"""HttpRequest(${_1},${_2},-,-,${_5})"""
override def toString = s"""HttpRequest(${_1},${_2},${redactedHeaders},${_4},${_5})"""

// name-based unapply accessors
def _1 = method
Expand All @@ -371,6 +371,8 @@ final class HttpRequest(
def _4 = entity
def _5 = protocol

private def redactedHeaders = headers.map(_.safeToString)

}

object HttpRequest {
Expand Down Expand Up @@ -509,14 +511,16 @@ final class HttpResponse(
result
}

override def toString = s"""HttpResponse(${_1},-,-,${_4})"""
override def toString = s"""HttpResponse(${_1},${redactedHeaders},${_3},${_4})"""

// name-based unapply accessors
def _1 = this.status
def _2 = this.headers
def _3 = this.entity
def _4 = this.protocol

private def redactedHeaders = headers.map(_.safeToString)

}

object HttpResponse {
Expand Down
Expand Up @@ -65,6 +65,9 @@ sealed trait ModeledHeader extends HttpHeader with Serializable {
final def render[R <: Rendering](r: R): r.type = renderValue(r ~~ companion)
protected[http] def renderValue[R <: Rendering](r: R): r.type
protected def companion: ModeledCompanion[_]

/* All modeled headers are represented un-redacted by default. */
override def safeToString: String = toString
}

private[headers] sealed trait RequestHeader extends ModeledHeader { override def renderInRequests = true }
Expand Down Expand Up @@ -381,6 +384,9 @@ object Authorization extends ModeledCompanion[Authorization]
final case class Authorization(credentials: HttpCredentials) extends jm.headers.Authorization with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ credentials
protected def companion = Authorization

// This header is tagged as potentially containing personal sensitive information
override def safeToString: String = name
}

// http://tools.ietf.org/html/rfc7234#section-5.2
Expand Down Expand Up @@ -525,6 +531,9 @@ final case class Cookie(cookies: immutable.Seq[HttpCookiePair]) extends jm.heade

/** Java API */
def getCookies: Iterable[jm.headers.HttpCookiePair] = cookies.asJava

// This header is tagged as potentially containing personal sensitive information
override def safeToString: String = name
}

// http://tools.ietf.org/html/rfc7231#section-7.1.1.2
Expand Down Expand Up @@ -589,6 +598,9 @@ final case class Host(host: Uri.Host, port: Int = 0) extends jm.headers.Host wit
def renderValue[R <: Rendering](r: R): r.type = if (port > 0) r ~~ host ~~ ':' ~~ port else r ~~ host
protected def companion = Host
def equalsIgnoreCase(other: Host): Boolean = host.equalsIgnoreCase(other.host) && port == other.port

// This header is tagged as potentially containing personal sensitive information
override def safeToString: String = name
}

// http://tools.ietf.org/html/rfc7232#section-3.1
Expand Down Expand Up @@ -697,6 +709,9 @@ final case class Origin(origins: immutable.Seq[HttpOrigin]) extends jm.headers.O

/** Java API */
def getOrigins: Iterable[jm.headers.HttpOrigin] = origins.asJava

// This header is tagged as potentially containing personal sensitive information
override def safeToString: String = name
}

// http://tools.ietf.org/html/rfc7235#section-4.3
Expand All @@ -721,6 +736,9 @@ final case class `Proxy-Authorization`(credentials: HttpCredentials) extends jm.
with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ credentials
protected def companion = `Proxy-Authorization`

// This header is tagged as potentially containing personal sensitive information
override def safeToString: String = name
}

// http://tools.ietf.org/html/rfc7233#section-3.1
Expand Down Expand Up @@ -763,6 +781,9 @@ object `Remote-Address` extends ModeledCompanion[`Remote-Address`]
final case class `Remote-Address`(address: RemoteAddress) extends jm.headers.RemoteAddress with SyntheticHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ address
protected def companion = `Remote-Address`

// This header is tagged as potentially containing personal sensitive information
override def safeToString: String = name
}

// http://tools.ietf.org/html/rfc7231#section-5.5.2
Expand All @@ -776,6 +797,9 @@ final case class Referer(uri: Uri) extends jm.headers.Referer with RequestHeader

/** Java API */
def getUri: akka.http.javadsl.model.Uri = uri.asJava

// This header is tagged as potentially containing personal sensitive information
override def safeToString: String = name
}

object `Retry-After` extends ModeledCompanion[`Retry-After`] {
Expand Down Expand Up @@ -1083,6 +1107,9 @@ final case class `X-Forwarded-For`(addresses: immutable.Seq[RemoteAddress]) exte

/** Java API */
def getAddresses: Iterable[jm.RemoteAddress] = addresses.asJava

// This header is tagged as potentially containing personal sensitive information
override def safeToString: String = name
}

object `X-Forwarded-Host` extends ModeledCompanion[`X-Forwarded-Host`] {
Expand All @@ -1101,6 +1128,9 @@ final case class `X-Forwarded-Host`(host: Uri.Host) extends jm.headers.XForwarde

/** Java API */
def getHost: jm.Host = host.asJava

// This header is tagged as potentially containing personal sensitive information
override def safeToString: String = name
}

object `X-Forwarded-Proto` extends ModeledCompanion[`X-Forwarded-Proto`]
Expand All @@ -1127,4 +1157,7 @@ final case class `X-Real-Ip`(address: RemoteAddress) extends jm.headers.XRealIp
import `X-Real-Ip`.addressRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ address
protected def companion = `X-Real-Ip`

// This header is tagged as potentially containing personal sensitive information
override def safeToString: String = name
}
Expand Up @@ -43,7 +43,7 @@ class DebuggingDirectivesSpec extends RoutingSpec {
resetDebugMsg()
Get("/hello") ~> route ~> check {
response shouldEqual Ok
normalizedDebugMsg shouldEqual "1: HttpRequest(HttpMethod(GET),http://example.com/hello,-,-,HttpProtocol(HTTP/1.1))\n"
normalizedDebugMsg shouldEqual "1: HttpRequest(HttpMethod(GET),http://example.com/hello,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1))\n"
}
}
}
Expand All @@ -58,7 +58,7 @@ class DebuggingDirectivesSpec extends RoutingSpec {
resetDebugMsg()
Get("/hello") ~> route ~> check {
response shouldEqual Ok
normalizedDebugMsg shouldEqual "2: Complete(HttpResponse(200 OK,-,-,HttpProtocol(HTTP/1.1)))\n"
normalizedDebugMsg shouldEqual "2: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1)))\n"
}
}
}
Expand All @@ -75,8 +75,8 @@ class DebuggingDirectivesSpec extends RoutingSpec {
response shouldEqual Ok
normalizedDebugMsg shouldEqual
"""|3: Response for
| Request : HttpRequest(HttpMethod(GET),http://example.com/hello,-,-,HttpProtocol(HTTP/1.1))
| Response: Complete(HttpResponse(200 OK,-,-,HttpProtocol(HTTP/1.1)))
| Request : HttpRequest(HttpMethod(GET),http://example.com/hello,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1))
| Response: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1)))
|""".stripMarginWithNewline("\n")
}
}
Expand All @@ -95,7 +95,7 @@ class DebuggingDirectivesSpec extends RoutingSpec {
Get("/hello") ~> route ~> check {
handled shouldBe false
normalizedDebugMsg shouldEqual
"""Request: HttpRequest(HttpMethod(GET),http://example.com/hello,-,-,HttpProtocol(HTTP/1.1))
"""Request: HttpRequest(HttpMethod(GET),http://example.com/hello,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1))
|was rejected with rejections:
|List(ValidationRejection(The request could not be validated,None))
|""".stripMargin
Expand Down
Expand Up @@ -41,7 +41,8 @@ class HttpRequestDetailedStringExampleSpec extends AkkaSpec {

// ... while default `toString` doesn't.
assert(!s"$httpRequestWithHeadersAndBody".contains(piiHeader.toString))
assert(!s"$httpRequestWithHeadersAndBody".contains(piiBody.toString))
// TODO: Uncomment once https://github.com/akka/akka-http/pull/2737 gets merged
//assert(!s"$httpRequestWithHeadersAndBody".contains(piiBody.toString))
}

}
Expand Up @@ -42,7 +42,8 @@ class HttpResponseDetailedStringExampleSpec extends AkkaSpec {

// ... while default `toString` doesn't.
assert(!s"$httpResponseWithHeadersAndBody".contains(piiHeader.toString))
assert(!s"$httpResponseWithHeadersAndBody".contains(piiBody.toString))
// TODO: Uncomment once https://github.com/akka/akka-http/pull/2737 gets merged
//assert(!s"$httpResponseWithHeadersAndBody".contains(piiBody.toString))
}

}

0 comments on commit dfb6474

Please sign in to comment.