From 24fb645fa949ece7d7e10363b77cf2d5fa2c2469 Mon Sep 17 00:00:00 2001 From: Milan Crha Date: Fri, 4 Sep 2026 10:34:22 +0200 Subject: [PATCH] cache: Reject caching authenticated responses in shared caches RFC 7234 Section 3.2 forbids to save to a shared cache a response to a request containing an Authorization header unless the response explicitly allows it with public, must-revalidate or s-maxage. This is CVE-2026-15713. Closes https://gitlab.gnome.org/GNOME/libsoup/-/work_items/541 --- libsoup/cache/soup-cache.c | 15 +++++++++++ tests/cache-test.c | 55 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/libsoup/cache/soup-cache.c b/libsoup/cache/soup-cache.c index 47e1ee70..5225382d 100644 --- a/libsoup/cache/soup-cache.c +++ b/libsoup/cache/soup-cache.c @@ -174,6 +174,7 @@ get_cacheability (SoupCache *cache, SoupMessage *msg) SoupCacheability cacheability; const char *cache_control, *content_type; gboolean has_max_age = FALSE; + gboolean permits_authorized_shared_caching = FALSE; /* 1. The request method must be cacheable */ if (soup_message_get_method (msg) == SOUP_METHOD_GET) @@ -201,6 +202,11 @@ get_cacheability (SoupCache *cache, SoupMessage *msg) soup_header_free_param_list (hash); return SOUP_CACHE_UNCACHEABLE; } + + permits_authorized_shared_caching = + g_hash_table_lookup_extended (hash, "public", NULL, NULL) || + g_hash_table_lookup_extended (hash, "must-revalidate", NULL, NULL) || + g_hash_table_lookup_extended (hash, "s-maxage", NULL, NULL); } /* 2. The 'no-store' cache directive does not appear in the @@ -225,6 +231,15 @@ get_cacheability (SoupCache *cache, SoupMessage *msg) soup_header_free_param_list (hash); } + /* RFC 7234 Section 3.2: a shared cache MUST NOT store a response to + * a request containing an Authorization header unless the response + * explicitly allows it with public, must-revalidate or s-maxage. + */ + if (priv->cache_type == SOUP_CACHE_SHARED && + !permits_authorized_shared_caching && + soup_message_headers_get_one_common (soup_message_get_request_headers (msg), SOUP_HEADER_AUTHORIZATION)) + return SOUP_CACHE_UNCACHEABLE; + /* Section 13.9 */ if ((g_uri_get_query (soup_message_get_uri (msg))) && !soup_message_headers_get_one_common (soup_message_get_response_headers (msg), SOUP_HEADER_EXPIRES) && diff --git a/tests/cache-test.c b/tests/cache-test.c index ef2ebeb3..dff6bd35 100644 --- a/tests/cache-test.c +++ b/tests/cache-test.c @@ -95,6 +95,7 @@ server_callback (SoupServer *server, if (status == SOUP_STATUS_OK) { GChecksum *sum; const char *body; + const char *authorization; sum = g_checksum_new (G_CHECKSUM_SHA256); g_checksum_update (sum, (guchar *)path, strlen (path)); @@ -102,6 +103,9 @@ server_callback (SoupServer *server, g_checksum_update (sum, (guchar *)last_modified, strlen (last_modified)); if (etag) g_checksum_update (sum, (guchar *)etag, strlen (etag)); + authorization = soup_message_headers_get_one (request_headers, "Authorization"); + if (authorization) + g_checksum_update (sum, (guchar *)authorization, strlen (authorization)); body = g_checksum_get_string (sum); soup_server_message_set_response (msg, "text/plain", SOUP_MEMORY_COPY, @@ -736,6 +740,56 @@ do_leaks_test (gconstpointer data) g_free (cache_dir); } +static void +do_shared_cache_authorization_test (gconstpointer data) +{ + GUri *base_uri = (GUri *)data; + SoupSession *attacker_session; + SoupSession *victim_session; + SoupCache *attacker_cache; + SoupCache *victim_cache; + char *cache_dir; + char *attacker_body; + char *victim_body; + + cache_dir = g_dir_make_tmp ("cache-test-XXXXXX", NULL); + debug_printf (2, " Caching to %s\n", cache_dir); + + attacker_cache = soup_cache_new (cache_dir, SOUP_CACHE_SHARED); + attacker_session = soup_test_session_new (NULL); + soup_session_add_feature (attacker_session, SOUP_SESSION_FEATURE (attacker_cache)); + + attacker_body = do_request (attacker_session, base_uri, "GET", "/authz-shared-cache", NULL, + "Authorization", "Bearer ATTACKER_SECRET", + "Test-Set-Cache-Control", "max-age=600", + NULL); + g_assert_true (last_request_hit_network); + + soup_cache_dump (attacker_cache); + soup_test_session_abort_unref (attacker_session); + g_object_unref (attacker_cache); + + victim_cache = soup_cache_new (cache_dir, SOUP_CACHE_SHARED); + soup_cache_load (victim_cache); + victim_session = soup_test_session_new (NULL); + soup_session_add_feature (victim_session, SOUP_SESSION_FEATURE (victim_cache)); + + victim_body = do_request (victim_session, base_uri, "GET", "/authz-shared-cache", NULL, + "Authorization", "Bearer VICTIM_SECRET", + "Test-Set-Cache-Control", "max-age=600", + NULL); + + g_assert_true (last_request_hit_network); + g_assert_cmpstr (attacker_body, !=, victim_body); + + g_free (attacker_body); + g_free (victim_body); + + soup_test_session_abort_unref (victim_session); + g_object_unref (victim_cache); + g_free (cache_dir); +} + static void do_metrics_test (gconstpointer data) { @@ -1108,6 +1162,7 @@ main (int argc, char **argv) g_test_add_data_func ("/cache/cancellation", base_uri, do_cancel_test); g_test_add_data_func ("/cache/refcounting", base_uri, do_refcounting_test); g_test_add_data_func ("/cache/headers", base_uri, do_headers_test); + g_test_add_data_func ("/cache/shared-cache-authorization", base_uri, do_shared_cache_authorization_test); g_test_add_data_func ("/cache/leaks", base_uri, do_leaks_test); g_test_add_data_func ("/cache/metrics", base_uri, do_metrics_test); g_test_add_data_func ("/cache/threads", base_uri, do_threads_test); -- GitLab