tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit fdbf27485b14b3e81f9cb5ffd3fa2d9b2776709a
parent ba17dce2c2a39b4f4d15f28a07ceb1f92191711c
Author: Valentin Gosu <valentin.gosu@gmail.com>
Date:   Tue,  7 Oct 2025 09:52:27 +0000

Bug 1937766 - Make sure pragma:no-cache is respected even when a cache-control header is present r=necko-reviewers,kershaw,emilio

Even though the pragma: no-cache header is deprecated according to
https://httpwg.org/specs/rfc9111.html#field.pragma this header is often
present on the web, sometimes conflicting with the cache-control header.

Normally cache-control would take precedence when there's a conflict,
but since other browsers treat pragma: no-cache as a definite signal to not
cache, a different behaviour risks webcompat issues.

We can probably make an exception for cache-control: immutable. Since this is
a signal that the resource is unlikely to change, it's probably safe to treat
resources as cacheable in this case.

Differential Revision: https://phabricator.services.mozilla.com/D266972

Diffstat:
Mnetwerk/protocol/http/nsHttpResponseHead.h | 11+++++++++--
Mnetwerk/test/gtest/TestHttpResponseHead.cpp | 25+++++++++++++++++++++++--
Mtesting/web-platform/tests/fetch/http-cache/pragma-no-cache-with-cache-control.html | 32++++++++++++++++++++++++++++----
Atesting/web-platform/tests/fetch/http-cache/resources/cached_pragma_immutable_rand.py | 14++++++++++++++
4 files changed, 74 insertions(+), 8 deletions(-)

diff --git a/netwerk/protocol/http/nsHttpResponseHead.h b/netwerk/protocol/http/nsHttpResponseHead.h @@ -198,9 +198,16 @@ class nsHttpResponseHead { } bool NoCache_locked() const MOZ_REQUIRES(mRecursiveMutex) { - // We ignore Pragma: no-cache if Cache-Control is set. MOZ_ASSERT_IF(mCacheControlNoCache, mHasCacheControl); - return mHasCacheControl ? mCacheControlNoCache : mPragmaNoCache; + // Normally we would ignore Pragma: no-cache if Cache-Control is set. + // But since all other browsers treat the existence of Pragma: no-cache + // as a signal to not-cache even when it conflicts with Cache-Control + // it is safer just to do the same. Previous behaviour where Pragma + // was ignored when Cache-Control was present resulted in several + // web-compat issues (Bug 1937766) + // However, the presence of cacheControl immutable indicates that + // pragma: no-cache is incorrectly added to the response. + return (mPragmaNoCache && !mCacheControlImmutable) || mCacheControlNoCache; } private: diff --git a/netwerk/test/gtest/TestHttpResponseHead.cpp b/netwerk/test/gtest/TestHttpResponseHead.cpp @@ -67,7 +67,8 @@ TEST(TestHttpResponseHead, bug1649807) Unused << head.ParseStatusLine("HTTP/1.1 200 OK"_ns); Unused << head.ParseHeaderLine("content-type: text/plain"_ns); Unused << head.ParseHeaderLine("etag: Just testing"_ns); - Unused << head.ParseHeaderLine("cache-control: age=99999"_ns); + Unused << head.ParseHeaderLine( + "cache-control: public, max-age=31536000, immutable"_ns); Unused << head.ParseHeaderLine("accept-ranges: bytes"_ns); Unused << head.ParseHeaderLine("content-length: 1408"_ns); Unused << head.ParseHeaderLine("connection: close"_ns); @@ -76,7 +77,27 @@ TEST(TestHttpResponseHead, bug1649807) Unused << head.ParseHeaderLine("date: Tue, 12 May 2020 09:24:23 GMT"_ns); ASSERT_FALSE(head.NoCache()) - << "Cache-Control wins over Pragma: no-cache"; + << "Cache-Control: immutable wins over Pragma: no-cache"; + AssertRoundTrips(head); +} + +TEST(TestHttpResponseHead, bug1937766) +{ + nsHttpResponseHead head; + + Unused << head.ParseStatusLine("HTTP/1.1 200 OK"_ns); + Unused << head.ParseHeaderLine("content-type: text/plain"_ns); + Unused << head.ParseHeaderLine("etag: Just testing"_ns); + Unused << head.ParseHeaderLine("cache-control: age=99999"_ns); + Unused << head.ParseHeaderLine("accept-ranges: bytes"_ns); + Unused << head.ParseHeaderLine("content-length: 1408"_ns); + Unused << head.ParseHeaderLine("connection: close"_ns); + Unused << head.ParseHeaderLine("server: httpd.js"_ns); + Unused << head.ParseHeaderLine("pragma: no-cache"_ns); + Unused << head.ParseHeaderLine("date: Tue, 12 May 2020 09:24:23 GMT"_ns); + + ASSERT_TRUE(head.NoCache()) + << "Pragma: no-cache wins over Cache-Control"; AssertRoundTrips(head); } diff --git a/testing/web-platform/tests/fetch/http-cache/pragma-no-cache-with-cache-control.html b/testing/web-platform/tests/fetch/http-cache/pragma-no-cache-with-cache-control.html @@ -5,11 +5,16 @@ <script src="/resources/testharnessreport.js"></script> <body> <script> -promise_test(async t => { +promise_test(async _t => { // According to https://www.rfc-editor.org/rfc/rfc9111.html#name-pragma // the pragma header is deprecated. // When there's a mismatch between pragma and Cache-Control then the latter - // should be respected, and the resource should be cached. + // should be respected, and the resource should be cached, but this could + // lead to web-compat issues when one browser caches and others don't. + // It's safer to avoid caching when Pragma: no-cache is present, even though + // this further ossifies its use. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1937766 and + // https://issues.chromium.org/issues/447171250 for discussion. const url = 'resources/cached_pragma_rand.py' // First fetch to populate the cache @@ -22,7 +27,26 @@ promise_test(async t => { assert_true(response2.ok, 'Second fetch should succeed'); const text2 = await response2.text(); - assert_equals(text1, text2, 'Responses should be identical, indicating caching'); -}, 'Response with Cache-Control: max-age=2592000, public and Pragma: no-cache should be cached'); + assert_not_equals(text1, text2, 'Responses should be different, indicating no cache use'); +}, 'Response with Cache-Control: max-age=2592000, public and Pragma: no-cache should not be cached'); + +promise_test(async _t => { + // Cache-Control: immutable should be cached even when Pragma: no-cache is present + // because immutable is a strong directive indicating the resource will never change. + const url = 'resources/cached_pragma_immutable_rand.py' + + // First fetch to populate the cache + const response1 = await fetch(url, { cache: 'default' }); + assert_true(response1.ok, 'First fetch should succeed'); + const text1 = await response1.text(); + + // Second fetch should be served from cache + const response2 = await fetch(url, { cache: 'default' }); + assert_true(response2.ok, 'Second fetch should succeed'); + const text2 = await response2.text(); + + assert_equals(text1, text2, 'Responses should be identical, indicating cache use'); +}, 'Response with Cache-Control: max-age=2592000, immutable and Pragma: no-cache should be cached'); + </script> </body> diff --git a/testing/web-platform/tests/fetch/http-cache/resources/cached_pragma_immutable_rand.py b/testing/web-platform/tests/fetch/http-cache/resources/cached_pragma_immutable_rand.py @@ -0,0 +1,14 @@ +def main(request, response): + # Disable non-standard XSS protection + response.headers.set(b"X-XSS-Protection", b"0") + response.headers.set(b"Content-Type", b"text/html") + + # Set caching headers with immutable directive + # Cache-Control: immutable indicates the resource will never change, + # and should be cached even when Pragma: no-cache is present. + response.headers.set(b"Cache-Control", b"max-age=2592000, immutable") + response.headers.set(b"Pragma", b"no-cache") + + # Include a timestamp to verify caching behavior + import time + response.content = f"Timestamp: {time.time()}".encode('utf-8')