Closed Bug 1981502 (CVE-2025-10536) Opened 9 months ago Closed 9 months ago

Cache-Control: no-store does not prevent disk cache entry when multiple headers are sent

Categories

(Core :: Networking: Cache, defect, P2)

Firefox 141
defect

Tracking

()

VERIFIED FIXED
144 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr128 --- wontfix
firefox-esr140 143+ verified
firefox142 --- wontfix
firefox143 + verified
firefox144 + verified

People

(Reporter: piipokun1703, Assigned: valentin)

Details

(4 keywords, Whiteboard: [necko-triaged][necko-priority-queue][adv-main143+][adv-esr140.3+])

Attachments

(6 files)

User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36

Steps to reproduce:

1.Start a local HTTP server using the attached server.py (Flask).

2.Navigate to http://localhost:8082/test7 in Firefox.

3.This endpoint responds with three separate Cache-Control headers:

ใƒปCache-Control: no-store
ใƒปCache-Control: public
ใƒปCache-Control: no-cache

4.Open about:cache.

5.Observe that an entry for /test7 appears in the disk cache.

6.This occurs despite no-store being present.

7.The response also includes a Set-Cookie: sessionid=test_token header.

firefox 141.0.2(64bit) Windows 11
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0

Actual results:

Firefox appears to honor only the last Cache-Control header.

If no-cache follows no-store, no-store is ignored.

The response ends up being cached.

The cache entry persists across browser restarts.

Expected results:

According to [RFC 9111 (Section 5.2.2.5)]the no-store directive prohibits intentional storage in non-volatile storage (e.g., disk), and instructs caches to make a best-effort attempt to avoid retaining the information even in volatile memory (e.g., RAM).

Thus, when a server sends Cache-Control: no-store, the browser:

  • MUST NOT store the response in disk cache (non-volatile).
  • SHOULD avoid memory caching, or at least remove the response from memory promptly.
  • MUST correctly process multiple Cache-Control headers and not ignore no-store even when combined with other directives.

Therefore, when multiple Cache-Control headers are present (e.g., no-store and no-cache), the browser should combine them appropriately and enforce the most restrictive directive โ€” which is no-store in this case.

Even when multiple Cache-Control headers are sent separately, the browser should merge them and respect the most restrictive directive, i.e., no-store.

Attached image disk_cache โ€”
Attached image disk_cache_test7 โ€”
Attached file server.py โ€”
Group: firefox-core-security → network-core-security
Component: Untriaged → Networking: Cache
Product: Firefox → Core
Flags: needinfo?(kershaw)

I believe this behavior is by design.
Regardless of whether the header includes no-store or no-cache, Firefox may still write the data to the cache, but it wonโ€™t use the cached data when those directives are present.

Valentin, please correct me if Iโ€™m wrong. Thanks.

Flags: needinfo?(kershaw) → needinfo?(valentin.gosu)
Attached file (secure) โ€”

nsHttpResponseHead::ParseHeaderLine_locked parses each header as it comes in.
It then updates its own member variables based on that header.
For cache-control, it's important to reparse the merged version of the header
instead of the most recent one, otherwise we'll miss directives present in
previous headers.

Assignee: nobody → valentin.gosu
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true

Actually, if no-store is present we should not save the cache entry to disk (memory is fine).
We don't end up using it later though, but the fact that we save it to disk is still slightly problematic for privacy.
Not sure if I'd consider this a security issue though.

Flags: needinfo?(valentin.gosu)
Severity: -- → S3
Priority: -- → P2
Whiteboard: [necko-triaged]

Regardless of whether the header includes no-store or no-cache, Firefox may still write the data to the cache, but it wonโ€™t use the cached data when those directives are present.

That is wrong. no-store was created to be explicit because browsers and middle-boxes were "caching and not using" the no-cache ones. It's used for sensitive data that should never be written to disk or any permanent place

Local file-system disclosure is not super high on our threat model (except in Private Browsing) so the severity is low.

Whiteboard: [necko-triaged] → [necko-triaged][necko-priority-queue]
Pushed by valentin.gosu@gmail.com: https://github.com/mozilla-firefox/firefox/commit/a664230b4e2d https://hg.mozilla.org/integration/autoland/rev/e214aeee4733 Parse merged version of cache-control instead of just the latest header r=necko-reviewers,jesup
Flags: sec-bounty?
Group: network-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 9 months ago
Resolution: --- → FIXED
Target Milestone: --- → 144 Branch

The patch landed in nightly and beta is affected.
:valentin, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(valentin.gosu)
Attached file (secure) โ€”

nsHttpResponseHead::ParseHeaderLine_locked parses each header as it comes in.
It then updates its own member variables based on that header.
For cache-control, it's important to reparse the merged version of the header
instead of the most recent one, otherwise we'll miss directives present in
previous headers.

Original Revision: https://phabricator.services.mozilla.com/D260461

Attachment #9509769 - Flags: approval-mozilla-beta?

firefox-beta Uplift Approval Request

  • User impact if declined: Cache-control: no-store would not be respected if it followed by other cache-control headers (probably rare in practice)
  • Code covered by automated testing: yes
  • Fix verified in Nightly: yes
  • Needs manual QE test: yes
  • Steps to reproduce for manual QE testing: Manual testing is optional, but see comment 3 for a test server
  • Risk associated with taking this patch: low
  • Explanation of risk level: This patch makes sure that the cache control parses the merged header, not just the most recent received cache-control header.
  • String changes made/needed: none
  • Is Android affected?: yes
Flags: qe-verify+

(In reply to BugBot [:suhaib / :marco/ :calixte] from comment #11)

:valentin, is this bug important enough to require an uplift?

The impact on users is probably small, but I've requested uplift anyway for corectness.

Flags: needinfo?(valentin.gosu)
Flags: in-testsuite+
Attachment #9509769 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Flags: sec-bounty? → sec-bounty+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c144/b143] [qa-ver-needed-c144/b143]

Reproduced the issue described in comment 0 using an old Nightly build from 2025-08-06.
Verified that:

  • using latest Nightly 144.0a1 http://localhost:8082/test7 is not even in the list in about:cache?storage=disk
  • using latest Beta 143 build from treeherder http://localhost:8082/test7 is listed in about:cache?storage=disk` but it has 0 date in it also there are two other entries (see bellow). The favicon.ico one is the only one displayed in Nightly as well.
http://localhost:8082/test7 | 0 bytes | 0 bytes | 0 | 2025-08-28 16:38:37 | No expiration time

http://localhost:8082/favicon.ico O^partitionKey=%28http%2Clocalhost%29, | 207 bytes | 0 bytes | 1 | 2025-08-28 16:38:09 | Expired Immediately

predictor-origin:http://localhost:8082/ | 0 bytes | 0 bytes | 2 | 2025-08-28 16:38:09 | No expiration time

I verified across platforms (Windows 11, macOS 13 and Ubuntu 22.04).
Not sure if the above entries are to be displayed in the Beta build or not even though except the favico one they have 0 data in it. Nightly only displays the favicon.ico one.

Flags: needinfo?(valentin.gosu)

The icon entry doesn't matter for this bug.
I think both the predictor and /test7 entry should go away if you set network.predictor.enabled to false
This is already the default for release and was changed to false in Bug 1979415.

Flags: needinfo?(valentin.gosu)

(In reply to Valentin Gosu [:valentin] (he/him) from comment #17)

I think both the predictor and /test7 entry should go away if you set network.predictor.enabled to false
This is already the default for release and was changed to false in Bug 1979415.

Indeed the entries are gone if setting network.predictor.enabled to false, thanks for the extra info.
Closing this as verified fixed for 144 and 143. Will also come back and verify once/if the fix goes into 140esr.

QA Whiteboard: [sec] [uplift] [qa-triage-done-c144/b143] [qa-ver-needed-c144/b143] → [sec] [uplift] [qa-triage-done-c144/b143] [qa-ver-done-c144/b143]
Flags: qe-verify+
Attached file (secure) โ€”

nsHttpResponseHead::ParseHeaderLine_locked parses each header as it comes in.
It then updates its own member variables based on that header.
For cache-control, it's important to reparse the merged version of the header
instead of the most recent one, otherwise we'll miss directives present in
previous headers.

Original Revision: https://phabricator.services.mozilla.com/D260461

Attachment #9510639 - Flags: approval-mozilla-esr140?

firefox-esr140 Uplift Approval Request

  • User impact if declined: Cache-control: no-store would not be respected if it followed by other cache-control headers (probably rare in practice)
  • Code covered by automated testing: yes
  • Fix verified in Nightly: yes
  • Needs manual QE test: yes
  • Steps to reproduce for manual QE testing: Manual testing is optional, but see comment 3 for a test server and comment 17 for testing context
  • Risk associated with taking this patch: low
  • Explanation of risk level: This patch makes sure that the cache control parses the merged header, not just the most recent received cache-control header.
  • String changes made/needed: none
  • Is Android affected?: yes
Flags: qe-verify+
Attachment #9510639 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+

Also verified as fixed using latest esr140 build from treeherder across platforms (Windows 11, macOS 13 and Ubuntu 22.04).

Status: RESOLVED → VERIFIED
Flags: qe-verify+
Whiteboard: [necko-triaged][necko-priority-queue] → [necko-triaged][necko-priority-queue][adv-main143+]
Whiteboard: [necko-triaged][necko-priority-queue][adv-main143+] → [necko-triaged][necko-priority-queue][adv-main143+][adv-esr140.3+]
Alias: CVE-2025-10536
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: