CVE-2024-49580
Description
In JetBrains Ktor before 2.3.13 improper caching in HttpCache Plugin could lead to response information disclosure
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
In JetBrains Ktor before 2.3.13, the HttpCache plugin improperly cached responses for requests with Authorization headers, leading to potential information disclosure.
Vulnerability
Description
The HttpCache plugin in JetBrains Ktor versions prior to 2.3.13 did not properly handle caching of responses for requests that include an Authorization header. According to HTTP caching specifications (RFC 7234, RFC 9111), responses to authenticated requests should not be cached by default to prevent leakage of sensitive data. The plugin's default configuration allowed caching such responses, violating these standards and exposing potentially sensitive information to other users or processes sharing the same cache [1][2].
Exploitation
Conditions
Exploitation requires an attacker to have access to the same cache storage used by the Ktor client. This could occur in shared environments such as multi-tenant servers or when multiple clients use a common cache backend. No authentication is needed beyond the ability to read the cache. The vulnerability is triggered when a Ktor client makes a request with an Authorization header (e.g., Bearer token) and the response is stored in the cache; subsequent requests that hit the cache may retrieve the cached response, including any sensitive data it contains [1][3].
Impact
An attacker with access to the shared cache can retrieve cached responses that were intended for authenticated users. This could lead to disclosure of sensitive information such as authentication tokens, session identifiers, or private data returned in the response body. The severity is heightened in environments where the cache is not isolated per user or session [2].
Mitigation
The issue is fixed in Ktor version 2.3.13. The fix introduces a new configuration option cacheRequestWithAuth (defaulting to false) that controls whether requests with Authorization headers are cached. Additionally, the plugin now respects the isSharedClient flag to determine caching behavior. Users are advised to upgrade to Ktor 2.3.13 or later. For those unable to upgrade, a workaround is to disable the HttpCache plugin or ensure the cache is not shared across different users [1][3].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.ktor:ktor-client-core-jvmMaven | < 2.3.13 | 2.3.13 |
Affected products
3Patches
20665736fc35cKTOR-7483 Allow auth header when client is not shared (#4368)
4 files changed · +25 −47
ktor-client/ktor-client-core/api/ktor-client-core.api+1 −3 modified@@ -598,7 +598,7 @@ public final class io/ktor/client/plugins/api/TransformResponseBodyContext { public final class io/ktor/client/plugins/cache/HttpCache { public static final field Companion Lio/ktor/client/plugins/cache/HttpCache$Companion; - public synthetic fun <init> (Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;Lio/ktor/client/plugins/cache/storage/CacheStorage;Lio/ktor/client/plugins/cache/storage/CacheStorage;ZZZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun <init> (Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;Lio/ktor/client/plugins/cache/storage/CacheStorage;Lio/ktor/client/plugins/cache/storage/CacheStorage;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V } public final class io/ktor/client/plugins/cache/HttpCache$Companion : io/ktor/client/plugins/HttpClientPlugin { @@ -612,13 +612,11 @@ public final class io/ktor/client/plugins/cache/HttpCache$Companion : io/ktor/cl public final class io/ktor/client/plugins/cache/HttpCache$Config { public fun <init> ()V - public final fun getCacheRequestWithAuth ()Z public final fun getPrivateStorage ()Lio/ktor/client/plugins/cache/storage/HttpCacheStorage; public final fun getPublicStorage ()Lio/ktor/client/plugins/cache/storage/HttpCacheStorage; public final fun isShared ()Z public final fun privateStorage (Lio/ktor/client/plugins/cache/storage/CacheStorage;)V public final fun publicStorage (Lio/ktor/client/plugins/cache/storage/CacheStorage;)V - public final fun setCacheRequestWithAuth (Z)V public final fun setPrivateStorage (Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;)V public final fun setPublicStorage (Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;)V public final fun setShared (Z)V
ktor-client/ktor-client-core/api/ktor-client-core.klib.api+0 −3 modified@@ -333,9 +333,6 @@ final class io.ktor.client.plugins.cache/HttpCache { // io.ktor.client.plugins.c final class Config { // io.ktor.client.plugins.cache/HttpCache.Config|null[0] constructor <init>() // io.ktor.client.plugins.cache/HttpCache.Config.<init>|<init>(){}[0] - final var cacheRequestWithAuth // io.ktor.client.plugins.cache/HttpCache.Config.cacheRequestWithAuth|{}cacheRequestWithAuth[0] - final fun <get-cacheRequestWithAuth>(): kotlin/Boolean // io.ktor.client.plugins.cache/HttpCache.Config.cacheRequestWithAuth.<get-cacheRequestWithAuth>|<get-cacheRequestWithAuth>(){}[0] - final fun <set-cacheRequestWithAuth>(kotlin/Boolean) // io.ktor.client.plugins.cache/HttpCache.Config.cacheRequestWithAuth.<set-cacheRequestWithAuth>|<set-cacheRequestWithAuth>(kotlin.Boolean){}[0] final var isShared // io.ktor.client.plugins.cache/HttpCache.Config.isShared|{}isShared[0] final fun <get-isShared>(): kotlin/Boolean // io.ktor.client.plugins.cache/HttpCache.Config.isShared.<get-isShared>|<get-isShared>(){}[0] final fun <set-isShared>(kotlin/Boolean) // io.ktor.client.plugins.cache/HttpCache.Config.isShared.<set-isShared>|<set-isShared>(kotlin.Boolean){}[0]
ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/cache/HttpCache.kt+22 −35 modified@@ -41,17 +41,18 @@ internal val LOGGER = KtorSimpleLogger("io.ktor.client.plugins.HttpCache") * You can learn more from [Caching](https://ktor.io/docs/client-caching.html). */ public class HttpCache private constructor( - @Deprecated("This will become internal", level = DeprecationLevel.ERROR) - @Suppress("DEPRECATION_ERROR") - internal val publicStorage: HttpCacheStorage, - @Deprecated("This will become internal", level = DeprecationLevel.ERROR) - @Suppress("DEPRECATION_ERROR") - internal val privateStorage: HttpCacheStorage, + @Deprecated( + "This will become internal", + level = DeprecationLevel.ERROR + ) @Suppress("DEPRECATION_ERROR") internal val publicStorage: HttpCacheStorage, + @Deprecated( + "This will become internal", + level = DeprecationLevel.ERROR + ) @Suppress("DEPRECATION_ERROR") internal val privateStorage: HttpCacheStorage, private val publicStorageNew: CacheStorage, private val privateStorageNew: CacheStorage, private val useOldStorage: Boolean, internal val isSharedClient: Boolean, - internal val cacheRequestWithAuth: Boolean ) { /** * A configuration for the [HttpCache] plugin. @@ -62,17 +63,6 @@ public class HttpCache private constructor( internal var privateStorageNew: CacheStorage = CacheStorage.Unlimited() internal var useOldStorage = false - /** - * Specifies if requests with Authorization header should be cached. - * - * According to specification, enabling this flag has security implications. - * See https://datatracker.ietf.org/doc/html/rfc2616#section-14.8, - * https://datatracker.ietf.org/doc/html/rfc7234#section-3, - * and https://datatracker.ietf.org/doc/html/rfc9111#section-3 for the details - */ - @Deprecated("Changing this flag has security implication", level = DeprecationLevel.WARNING) - public var cacheRequestWithAuth: Boolean = false - /** * Specifies if the client where this plugin is installed is shared among multiple users. * When set to true, all responses with `private` Cache-Control directive will not be cached. @@ -143,15 +133,13 @@ public class HttpCache private constructor( val config = Config().apply(block) with(config) { - @Suppress("DEPRECATION_ERROR") - return HttpCache( + @Suppress("DEPRECATION_ERROR") return HttpCache( publicStorage = publicStorage, privateStorage = privateStorage, publicStorageNew = publicStorageNew, privateStorageNew = privateStorageNew, useOldStorage = useOldStorage, - isSharedClient = isShared, - cacheRequestWithAuth = cacheRequestWithAuth + isSharedClient = isShared ) } } @@ -165,7 +153,7 @@ public class HttpCache private constructor( if (content !is OutgoingContent.NoContent) return@intercept if (context.method != HttpMethod.Get || !context.url.protocol.canStore()) return@intercept - if (!plugin.cacheRequestWithAuth && context.headers.contains(HttpHeaders.Authorization)) { + if (plugin.isSharedClient && context.headers.contains(HttpHeaders.Authorization)) { return@intercept } @@ -187,9 +175,8 @@ public class HttpCache private constructor( val validateStatus = shouldValidate(cache.expires, cache.headers, context) if (validateStatus == ValidateStatus.ShouldNotValidate) { - val cachedCall = cache - .createResponse(scope, RequestForCache(context.build()), context.executionContext) - .call + val cachedCall = + cache.createResponse(scope, RequestForCache(context.build()), context.executionContext).call proceedWithCache(scope, cachedCall) return@intercept } @@ -221,17 +208,19 @@ public class HttpCache private constructor( LOGGER.trace("Caching response for ${response.call.request.url}") val cachedData = plugin.cacheResponse(response) if (cachedData != null) { - val reusableResponse = cachedData - .createResponse(scope, response.request, response.coroutineContext) + val reusableResponse = + cachedData.createResponse(scope, response.request, response.coroutineContext) proceedWith(reusableResponse) return@intercept } } if (response.status == HttpStatusCode.NotModified) { LOGGER.trace("Not modified response for ${response.call.request.url}, replying from cache") - val responseFromCache = plugin.findAndRefresh(response.call.request, response) - ?: throw InvalidCacheStateException(response.call.request.url) + val responseFromCache = + plugin.findAndRefresh(response.call.request, response) ?: throw InvalidCacheStateException( + response.call.request.url + ) scope.monitor.raise(HttpResponseFromCache, responseFromCache) proceedWith(responseFromCache) @@ -340,11 +329,9 @@ public class HttpCache private constructor( else -> { val requestHeaders = mergedHeadersLookup(request.content, request.headers::get, request.headers::getAll) - storage.findAll(url) - .sortedByDescending { it.responseTime } - .firstOrNull { cachedResponse -> - cachedResponse.varyKeys.all { (key, value) -> requestHeaders(key) == value } - } + storage.findAll(url).sortedByDescending { it.responseTime }.firstOrNull { cachedResponse -> + cachedResponse.varyKeys.all { (key, value) -> requestHeaders(key) == value } + } } }
ktor-client/ktor-client-core/jvm/test/HttpCacheTest.kt+2 −6 modified@@ -70,7 +70,7 @@ class HttpCacheTest { } @Test - fun `should mix ETags when Authorization header is present and flag enabled`() = testApplication { + fun `should mix ETags when Authorization header is present and client is not shared`() = testApplication { application { routing { get("/me") { @@ -92,11 +92,7 @@ class HttpCacheTest { } val client = createClient { - install(HttpCache) { - isShared = true - @Suppress("DEPRECATION") - cacheRequestWithAuth = true - } + install(HttpCache) } assertEquals(
d6c3a51df169KTOR-7483 Avoid caching requests with Authorization header (#4337)
6 files changed · +153 −4
ktor-client/ktor-client-core/api/ktor-client-core.api+3 −1 modified@@ -598,7 +598,7 @@ public final class io/ktor/client/plugins/api/TransformResponseBodyContext { public final class io/ktor/client/plugins/cache/HttpCache { public static final field Companion Lio/ktor/client/plugins/cache/HttpCache$Companion; - public synthetic fun <init> (Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;Lio/ktor/client/plugins/cache/storage/CacheStorage;Lio/ktor/client/plugins/cache/storage/CacheStorage;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun <init> (Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;Lio/ktor/client/plugins/cache/storage/CacheStorage;Lio/ktor/client/plugins/cache/storage/CacheStorage;ZZZLkotlin/jvm/internal/DefaultConstructorMarker;)V } public final class io/ktor/client/plugins/cache/HttpCache$Companion : io/ktor/client/plugins/HttpClientPlugin { @@ -612,11 +612,13 @@ public final class io/ktor/client/plugins/cache/HttpCache$Companion : io/ktor/cl public final class io/ktor/client/plugins/cache/HttpCache$Config { public fun <init> ()V + public final fun getCacheRequestWithAuth ()Z public final fun getPrivateStorage ()Lio/ktor/client/plugins/cache/storage/HttpCacheStorage; public final fun getPublicStorage ()Lio/ktor/client/plugins/cache/storage/HttpCacheStorage; public final fun isShared ()Z public final fun privateStorage (Lio/ktor/client/plugins/cache/storage/CacheStorage;)V public final fun publicStorage (Lio/ktor/client/plugins/cache/storage/CacheStorage;)V + public final fun setCacheRequestWithAuth (Z)V public final fun setPrivateStorage (Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;)V public final fun setPublicStorage (Lio/ktor/client/plugins/cache/storage/HttpCacheStorage;)V public final fun setShared (Z)V
ktor-client/ktor-client-core/api/ktor-client-core.klib.api+3 −0 modified@@ -333,6 +333,9 @@ final class io.ktor.client.plugins.cache/HttpCache { // io.ktor.client.plugins.c final class Config { // io.ktor.client.plugins.cache/HttpCache.Config|null[0] constructor <init>() // io.ktor.client.plugins.cache/HttpCache.Config.<init>|<init>(){}[0] + final var cacheRequestWithAuth // io.ktor.client.plugins.cache/HttpCache.Config.cacheRequestWithAuth|{}cacheRequestWithAuth[0] + final fun <get-cacheRequestWithAuth>(): kotlin/Boolean // io.ktor.client.plugins.cache/HttpCache.Config.cacheRequestWithAuth.<get-cacheRequestWithAuth>|<get-cacheRequestWithAuth>(){}[0] + final fun <set-cacheRequestWithAuth>(kotlin/Boolean) // io.ktor.client.plugins.cache/HttpCache.Config.cacheRequestWithAuth.<set-cacheRequestWithAuth>|<set-cacheRequestWithAuth>(kotlin.Boolean){}[0] final var isShared // io.ktor.client.plugins.cache/HttpCache.Config.isShared|{}isShared[0] final fun <get-isShared>(): kotlin/Boolean // io.ktor.client.plugins.cache/HttpCache.Config.isShared.<get-isShared>|<get-isShared>(){}[0] final fun <set-isShared>(kotlin/Boolean) // io.ktor.client.plugins.cache/HttpCache.Config.isShared.<set-isShared>|<set-isShared>(kotlin.Boolean){}[0]
ktor-client/ktor-client-core/build.gradle.kts+1 −0 modified@@ -40,6 +40,7 @@ kotlin.sourceSets { dependencies { api(project(":ktor-test-dispatcher")) api(project(":ktor-client:ktor-client-mock")) + api(project(":ktor-server:ktor-server-test-host")) } } }
ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/cache/HttpCache.kt+19 −2 modified@@ -50,7 +50,8 @@ public class HttpCache private constructor( private val publicStorageNew: CacheStorage, private val privateStorageNew: CacheStorage, private val useOldStorage: Boolean, - internal val isSharedClient: Boolean + internal val isSharedClient: Boolean, + internal val cacheRequestWithAuth: Boolean ) { /** * A configuration for the [HttpCache] plugin. @@ -61,6 +62,17 @@ public class HttpCache private constructor( internal var privateStorageNew: CacheStorage = CacheStorage.Unlimited() internal var useOldStorage = false + /** + * Specifies if requests with Authorization header should be cached. + * + * According to specification, enabling this flag has security implications. + * See https://datatracker.ietf.org/doc/html/rfc2616#section-14.8, + * https://datatracker.ietf.org/doc/html/rfc7234#section-3, + * and https://datatracker.ietf.org/doc/html/rfc9111#section-3 for the details + */ + @Deprecated("Changing this flag has security implication", level = DeprecationLevel.WARNING) + public var cacheRequestWithAuth: Boolean = false + /** * Specifies if the client where this plugin is installed is shared among multiple users. * When set to true, all responses with `private` Cache-Control directive will not be cached. @@ -138,7 +150,8 @@ public class HttpCache private constructor( publicStorageNew = publicStorageNew, privateStorageNew = privateStorageNew, useOldStorage = useOldStorage, - isSharedClient = isShared + isSharedClient = isShared, + cacheRequestWithAuth = cacheRequestWithAuth ) } } @@ -152,6 +165,10 @@ public class HttpCache private constructor( if (content !is OutgoingContent.NoContent) return@intercept if (context.method != HttpMethod.Get || !context.url.protocol.canStore()) return@intercept + if (!plugin.cacheRequestWithAuth && context.headers.contains(HttpHeaders.Authorization)) { + return@intercept + } + if (plugin.useOldStorage) { interceptSendLegacy(plugin, content, scope) return@intercept
ktor-client/ktor-client-core/jvm/test/HttpCacheTest.kt+127 −0 added@@ -0,0 +1,127 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import io.ktor.client.plugins.cache.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.testing.* +import kotlinx.coroutines.* +import kotlin.test.* +import kotlin.time.Duration.Companion.milliseconds + +class HttpCacheTest { + + @Test + fun `should not mix ETags when Authorization header is present`() = testApplication { + application { + routing { + get("/me") { + val user = call.request.headers["Authorization"]!! + if (user == "user-a") { + // Simulate slower network for one of the requests + delay(100.milliseconds) + } + val etag = "etag-of-$user" + if (call.request.headers["If-None-Match"] == etag) { + call.respond(HttpStatusCode.NotModified) + return@get + } + call.response.header("Cache-Control", "no-cache") + call.response.header("ETag", etag) + call.respondText(user) + } + } + } + + val client = createClient { + install(HttpCache) { + isShared = true + } + } + + assertEquals( + client.get("/me") { + headers["Authorization"] = "user-a" + }.bodyAsText(), + "user-a" + ) + withContext(Dispatchers.Default) { + listOf( + launch { + val response = client.get("/me") { + headers["Authorization"] = "user-a" + }.bodyAsText() + + assertEquals("user-a", response) + }, + launch { + val response = client.get("/me") { + headers["Authorization"] = "user-b" + }.bodyAsText() + + assertEquals("user-b", response) + } + ).joinAll() + } + } + + @Test + fun `should mix ETags when Authorization header is present and flag enabled`() = testApplication { + application { + routing { + get("/me") { + val user = call.request.headers["Authorization"]!! + if (user == "user-a") { + // Simulate slower network for one of the requests + delay(100.milliseconds) + } + val etag = "etag-of-$user" + if (call.request.headers["If-None-Match"] == etag) { + call.respond(HttpStatusCode.NotModified) + return@get + } + call.response.header("Cache-Control", "no-cache") + call.response.header("ETag", etag) + call.respondText(user) + } + } + } + + val client = createClient { + install(HttpCache) { + isShared = true + @Suppress("DEPRECATION") + cacheRequestWithAuth = true + } + } + + assertEquals( + client.get("/me") { + headers["Authorization"] = "user-a" + }.bodyAsText(), + "user-a" + ) + withContext(Dispatchers.Default) { + listOf( + launch { + val response = client.get("/me") { + headers["Authorization"] = "user-a" + }.bodyAsText() + + assertEquals("user-b", response) + }, + launch { + val response = client.get("/me") { + headers["Authorization"] = "user-b" + }.bodyAsText() + + assertEquals("user-b", response) + } + ).joinAll() + } + } +}
ktor-io/common/test/ByteReadChannelOperationsTest.kt+0 −1 modified@@ -82,5 +82,4 @@ class ByteReadChannelOperationsTest { assertContentEquals(expected.copyOfRange(0, 5), actual.copyOfRange(3, 8)) assertContentEquals(ByteArray(2) { 0 }, actual.copyOfRange(8, 10)) } - }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-8qv4-773j-c979ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-49580ghsaADVISORY
- github.com/ktorio/ktor/commit/0665736fc35c8ab5525241e975f36819b67f9d3eghsaWEB
- github.com/ktorio/ktor/commit/d6c3a51df169c163e8f0b9ce77bbe543c70116acghsaWEB
- github.com/ktorio/ktor/pull/4337ghsaWEB
- github.com/ktorio/ktor/pull/4368ghsaWEB
- github.com/ktorio/ktor/releases/tag/2.3.13ghsaWEB
- www.jetbrains.com/privacy-security/issues-fixedghsaWEB
- youtrack.jetbrains.com/issue/KTOR-7483ghsaWEB
- www.jetbrains.com/privacy-security/issues-fixed/mitre
News mentions
0No linked articles in our index yet.