https://bz.apache.org/bugzilla/show_bug.cgi?id=70028

            Bug ID: 70028
           Summary: mod_cache trunk regression: comma-separated
                    Cache-Control directives are not parsed correctly,
                    allowing private/no-store responses to be cached
           Product: Apache httpd-2
           Version: 2.5-HEAD
          Hardware: PC
                OS: All
            Status: NEW
          Severity: major
          Priority: P2
         Component: mod_cache
          Assignee: [email protected]
          Reporter: [email protected]
  Target Milestone: ---

This was originally reported to [email protected]. I was asked to open a
public Bugzilla issue because the impact appears to be trunk-only.

Summary:
While testing Apache httpd trunk / 2.5.1-dev, I found a regression in the
Cache-Control parser used by mod_cache.

A response containing multiple comma-separated Cache-Control directives such
as:

    Cache-Control: private, no-store, max-age=0

is not parsed correctly by trunk in my test build. As a result, mod_cache does
not set the expected cache_control_t flags:

    private = 1
    no_store = 1
    max_age = 1

and the response can be cached even when the configuration explicitly uses the
safe/default behaviour:

    CacheStorePrivate Off
    CacheStoreNoStore Off

This allows an authenticated private response to be stored by the shared
reverse proxy cache and later served to an anonymous request for the same URL.

This does not reproduce on the 2.4.x branch in my test: 2.4.x correctly rejects
the response with:

    Reason: Cache-Control: no-store present

Affected version tested:
    Apache/2.5.1-dev (Unix)
    trunk build

Unaffected comparison:
    Apache/2.4.68-dev (Unix)
    2.4.x branch

Relevant configuration:
    CacheRoot ".../cache"
    CacheEnable disk "/proxy/"
    CacheQuickHandler On
    CacheHeader on
    CacheLock on
    CacheStorePrivate Off
    CacheStoreNoStore Off
    CacheMaxFileSize 10000000
    CacheMinFileSize 1

Reproduction:
1. Run Apache httpd trunk / 2.5.1-dev as a reverse proxy with mod_cache and
mod_cache_disk enabled.

2. Configure caching for a proxied path, for example:

       CacheEnable disk "/proxy/"
       CacheStorePrivate Off
       CacheStoreNoStore Off
       CacheHeader on

3. Configure the backend to return a user/session-dependent response with:

       Cache-Control: private, no-store, max-age=0
       Vary: Host
       Last-Modified: Thu, 01 Jan 2026 00:00:00 GMT
       ETag: "CACHE-TEST"

4. First request the URL with an authenticated/session cookie:

       GET /proxy/admin/dashboard HTTP/1.1
       Host: public.localhost
       Cookie: session=VICTIM_ADMIN_SESSION

   The backend returns an authenticated/private response.

5. Then request the same URL without any Cookie header.

Expected result:
The first response must not be stored because it contains:

       Cache-Control: private, no-store, max-age=0

The anonymous request should go to the backend and receive the anonymous
response.

Actual result on trunk:
The private response is cached and later served to the anonymous request.

Evidence from my test:

Authenticated request:

    HTTP/1.1 200 OK
    Cache-Control: private, no-store, max-age=0
    Vary: Host
    X-Cache: MISS from public.localhost
    AUTHENTICATED_ADMIN_VIEW
    Session marker: session=VICTIM_ADMIN_SESSION
    CSRF token: CSRF_TOKEN_...
    API key preview: sk_live_ADMIN_SECRET_1777

Anonymous request to the same URL:

    HTTP/1.1 200 OK
    Cache-Control: private, no-store, max-age=0
    Vary: Host
    Age: 0
    X-Cache: HIT from public.localhost
    AUTHENTICATED_ADMIN_VIEW
    Session marker: session=VICTIM_ADMIN_SESSION
    CSRF token: CSRF_TOKEN_...
    API key preview: sk_live_ADMIN_SECRET_1777

Relevant cache log:

    AH00769: cache: Caching url
http://public.localhost:80/proxy/admin/dashboard? for request
/proxy/admin/dashboard
    AH00737: commit_entity: Headers and body for URL
http://public.localhost:80/proxy/admin/dashboard? cached.
    AH00764: cache: serving /proxy/admin/dashboard

Root cause analysis:
The regression appears to be in modules/cache/cache_util.c, in the trunk
implementation of cache_strqtok() / ap_cache_control().

In the trunk parser, the IN_TOKEN state does not stop when it reaches a token
separator such as comma or whitespace. Therefore a header such as:

    private, no-store, max-age=0

can be treated as a single token rather than separate directives. This causes
the comparisons for "private", "no-store", and "max-age" to fail.

In 2.4.x, the older parser correctly splits the Cache-Control header and sets
the expected flags.

I locally tested a minimal fix by stopping the IN_TOKEN state when
TEST_CHAR(*str, T_HTTP_TOKEN_STOP) is reached, before falling through to
argument parsing. After that change, trunk correctly parses:

    Cache-Control: private, no-store, max-age=0

and mod_cache refuses to store the response:

    DEBUG_CACHE_DECISION_POST: no_store=1 private=1 max_age=1 max_age_value=0
    AH00768: cache: ... not cached ... Reason: Cache-Control: no-store present

Suggested fix direction:
In cache_strqtok(), while in IN_TOKEN state, stop token parsing when a token
separator / HTTP token stop character is encountered, so that comma-separated
Cache-Control directives are parsed independently.

Example local patch direction:

    case IN_TOKEN:
        if (*str == '=') {
            state = IN_BETWEEN;
            *wpos++ = '\0';
            if (arg) *arg = wpos;
            continue;
        }
        if (TEST_CHAR(*str, T_HTTP_TOKEN_STOP)) {
            goto end;
        }
        break;

Security impact:
This is trunk-only based on my testing, so I understand this may not receive a
CVE. However, if this regression reached a stable release, a shared reverse
proxy cache could turn authenticated private responses into shared cache
objects retrievable by anonymous users.

This could expose admin dashboards, account pages, CSRF tokens, API key
previews, billing pages, /api/me responses, or other session-dependent content
whenever the backend relies on Cache-Control: private/no-store to prevent
shared caching.

I can provide the complete PoC lab files and command output if useful.

-- 
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to