Side Channel Attack in Chi with Basic Auth
Side Channel Attack in Chi with Basic Auth — how this specific combination creates or exposes the vulnerability
A side channel attack observes indirect signals—timing, length, or error behavior—to infer secret information. In Chi, using HTTP Basic Authentication over non-TLS channels is especially hazardous because the credentials are only base64-encoded, not encrypted. An attacker on the network path can observe request timing and length patterns to recover credentials or session correlation without directly breaking the hash.
Chi routes are compiled into a decision tree. If a route handling a token-sensitive operation (e.g., changing email or resetting a password) is placed before a route requiring authentication, an unauthenticated request may take a shorter path than an authenticated one. This measurable difference in response time can leak whether a particular token is valid, effectively turning Basic Auth’s static header into a side-channel identifier.
Consider a Chi service that decodes the Authorization header, validates credentials against a database, and then branches logic based on role or scope. Even with TLS, timing discrepancies in password hashing (e.g., different durations for valid versus invalid users) can be observable at the network layer. In Chi, a route structured as:
let authCheck = authenticate(req) match {
case Some(credentials) -> {
// role-based branching
if (credentials.scope == "admin") Route.adminRoute
else Route.userRoute
}
case None -> Route.publicRoute
};
can expose which branch executed through measurable latency. If the admin path involves additional middleware or database lookups, an attacker measuring response times can infer role membership. Moreover, if the service logs credentials or errors differently depending on authentication state, those logs become a side-channel artifact. Repeated requests with slightly altered headers can reveal average response differences, enabling statistical analysis to correlate timing with successful authentication.
Basic Auth exacerbates this because the same credentials are sent with every request. Without rotating nonces or session tokens, timing patterns remain consistent across requests, making it easier to build a profile. In a Chi application that does not constant-time-compare passwords or tokens, an attacker can craft requests that probe boundary conditions—such as empty passwords, wrong-length tokens, or malformed headers—and observe which conditions produce distinct timing deviations.
To mitigate, ensure all authenticated and unauthenticated paths have uniform processing time, avoid branching on sensitive data in hot paths, and enforce TLS to prevent on-path observation. Use constant-time comparison for secrets, and consider migrating from Basic Auth to token-based schemes where possible. middleBrick scans can surface these risks by correlating spec definitions with runtime behavior, highlighting inconsistencies between documented authentication requirements and actual execution paths.
Basic Auth-Specific Remediation in Chi — concrete code fixes
Remediation focuses on eliminating timing variability and ensuring credentials are handled uniformly. Below are concrete Chi examples that address side-channel risks while preserving Basic Auth for compatibility.
1. Constant-time comparison and uniform error handling:
import cats.effect.IO
import org.http4s._
import org.http4s.dsl.io._
import java.security.MessageDigest
import java.util.Base64
// Constant-time byte comparison to avoid timing leaks
def safeEquals(a: Array[Byte], b: Array[Byte]): Boolean = {
if (a.length != b.length) {
// Still process to completion to keep timing stable
MessageDigest.getInstance("SHA-256").digest(a) sameElements MessageDigest.getInstance("SHA-256").digest(b)
} else {
var result = 0
var i = 0
while (i < a.length) {
result |= a(i) ^ b(i)
i += 1
}
result == 0
}
}
def decodeBasic(authHeader: Option[Header]): IO[Option[(String, String)]] = authHeader match {
case Some(h) if h.name == `Authorization` =>
IO.fromOption(Option(new String(Base64.getDecoder.decode(h.value.drop("Basic ".length)))
.split(":", 2)).map { parts => (parts(0), parts(1)) })
case _ => IO.pure(None)
}
val authRoutes = HttpRoutes[IO] {
case req @ POST -> Root / "login" =>
for {
credsOpt ← decodeBasic(req.headers)
_ ← credsOpt match {
case Some((user, pass)) if safeEquals(user.getBytes("UTF-8"), "admin".getBytes("UTF-8")) && safeEquals(pass.getBytes("UTF-8"), "s3cret".getBytes("UTF-8")) =>
// Do work with fixed-cost validation before branching
IO.unit
case _ ->
// Return same status and similar processing time regardless of input
IO.unit
}
} yield Ok()
};
2. Route ordering and middleware normalization:
import cats.effect.IO
import org.http4s._
import org.http4s.server.Router
import org.http4s.server.middleware._
// Ensure public and authenticated routes share middleware for logging and error formatting
val publicRoutes = HttpRoutes[IO] {
case GET -> Root => Ok("public")
};
val privateRoutes = HttpRoutes[IO] {
case GET -> Root / "secure" => {
// Authentication enforced uniformly
val maybeCreds = decodeBasic(req.headers).value.unsafeRunSync()
maybeCreds match {
case Some(_) -> Ok("secure data")
case None -> Unauthorized()
}
}
};
// Combine with a prefix router; avoid branching on credentials in route selection
val httpApp = Router(
"" -> publicRoutes,
"api" -> privateRoutes
).orNotFound;
// Use middleware to normalize timing characteristics
val timedApp = Metrics[IO](new Slf4jLogger[IO]())(httpApp);
3. Enforce TLS and avoid Basic Auth over cleartext:
// In your server builder, enforce HTTPS
import org.http4s.server.blaze.BlazeServerBuilder
import cats.effect.IO
BlazeServerBuilder[IO]
.withHttpApp(timedApp)
.withSslContext { sslContext =
// Configure TLS context with strong ciphers; reject cleartext HTTP
}
.resource
.use(_ => IO.never)
.as(daemon)
.compile
.drain
.unsafeRunSync()
These changes reduce observable differences between authenticated and unauthenticated paths, use constant-time checks, and enforce encryption. middleBrick can validate that your spec declares TLS requirements and that runtime behavior aligns with authentication expectations, supporting a more secure posture without relying on Basic Auth in untrusted environments.