]> git.kaiwu.me - haproxy.git/commitdiff
BUG/MEDIUM: cache: ignore cache on redundant origin/referer
authorWilly Tarreau <w@1wt.eu>
Sun, 16 Aug 2026 06:09:32 +0000 (08:09 +0200)
committerWilly Tarreau <w@1wt.eu>
Wed, 26 Aug 2026 11:51:07 +0000 (13:51 +0200)
The cache's Vary header processing to consider secondary keys used only
the first occurrence of Origin and Referer. These header are single-valued
so this is not supposed to be a problem, however the server's behavior
when dealing with more than one will not necessarily be predictable, so
we'd rather just bypass the cache when facing such requests.

default_normalizer() now returns -1 if a second occurrence is found,
making http_request_build_secondary_key() return a failure and never
match from secondary keys. This way it doesn't affect the standard case
and doesn't take risks with border-line ones.

The vary regtest was completed with various combinations.

Reported-by: Claude (ANT-2026-FQEGP4YG)
This should be backported to all stable versions.

doc/configuration.txt
reg-tests/cache/vary.vtc
src/cache.c

index 89a08851166098b71ab0cee0ac56ed48f247093f..5a5dcf9c1e08e2cc3308a1721ba52c080cfa56a9 100644 (file)
@@ -20461,7 +20461,11 @@ process-vary <on/off>
   (which might come with a cpu cost) which will be used to build a secondary
   key for a given request (see RFC 7234#4.1). The secondary key is built out of
   the contents of the 'accept-encoding', 'referer' and 'origin' headers for
-  now. The default value is off (disabled).
+  now. Note that the 'origin' and 'referer' headers are single-valued per the
+  RFC, so a request carrying more than one occurrence of either should be
+  considered malformed. For such requests no secondary key is built and the
+  response is never delivered from the cache so that we can let the server
+  decide how to handle the situation. The default value is off (disabled).
 
 total-max-size <megabytes>
   Define the size in RAM of the cache in megabytes. This size is split in
index 5a6e6986c18ff0025a642937cb773360e9f28eab..4f34a122295cca00a5912ca66d4a69aa42268b00 100644 (file)
@@ -146,6 +146,43 @@ server s1 {
        txresp -hdr "Vary: accept-encoding" \
                -hdr "Cache-Control: max-age=5" \
                -bodylen 256
+
+       # Repeated Origin / Referer headers: duplicates are not cached.
+       rxreq
+       expect req.url == "/repeated-origin"
+       txresp -hdr "Vary: origin" \
+              -hdr "Cache-Control: max-age=5" \
+              -bodylen 270
+
+       rxreq
+       expect req.url == "/repeated-origin"
+       txresp -hdr "Vary: origin" \
+              -hdr "Cache-Control: max-age=5" \
+              -bodylen 271
+
+       rxreq
+       expect req.url == "/repeated-origin"
+       txresp -hdr "Vary: origin" \
+              -hdr "Cache-Control: max-age=5" \
+              -bodylen 281
+
+       rxreq
+       expect req.url == "/repeated-referer"
+       txresp -hdr "Vary: referer" \
+              -hdr "Cache-Control: max-age=5" \
+              -bodylen 292
+
+       rxreq
+       expect req.url == "/repeated-referer"
+       txresp -hdr "Vary: referer" \
+              -hdr "Cache-Control: max-age=5" \
+              -bodylen 292
+
+       rxreq
+       expect req.url == "/repeated-referer"
+       txresp -hdr "Vary: referer" \
+              -hdr "Cache-Control: max-age=5" \
+              -bodylen 303
 } -start
 
 server s2 {
@@ -496,6 +533,77 @@ client c1 -connect ${h1_fe_sock} {
        expect resp.http.content-encoding == "<undef>"
        expect resp.http.X-Cache-Hit == 0
 
+       # Repeated origin or referers are not cached since not valid.
+
+       # Two origins -> not cached, first request is a miss.
+       txreq -url "/repeated-origin" \
+               -hdr "Origin: origin_a" \
+               -hdr "Origin: origin_b"
+       rxresp
+       expect resp.status == 200
+       expect resp.bodylen == 270
+       expect resp.http.X-Cache-Hit == 0
+
+       # Same two origins again -> still a miss, the response was not stored.
+       txreq -url "/repeated-origin" \
+               -hdr "Origin: origin_a" \
+               -hdr "Origin: origin_b"
+       rxresp
+       expect resp.status == 200
+       expect resp.bodylen == 271
+       expect resp.http.X-Cache-Hit == 0
+
+       # A single origin is well-formed and must be cached normally, without
+       # being affected by the malformed requests above.
+       txreq -url "/repeated-origin" \
+               -hdr "Origin: origin_a"
+       rxresp
+       expect resp.status == 200
+       expect resp.bodylen == 281
+       expect resp.http.X-Cache-Hit == 0
+
+       # Same single origin again -> cache hit on its own entry.
+       txreq -url "/repeated-origin" \
+               -hdr "Origin: origin_a"
+       rxresp
+       expect resp.status == 200
+       expect resp.bodylen == 281
+       expect resp.http.X-Cache-Hit == 1
+
+       # Repeated Referer headers behave the same way.
+       txreq -url "/repeated-referer" \
+               -hdr "Referer: referer_a" \
+               -hdr "Referer: referer_b"
+       rxresp
+       expect resp.status == 200
+       expect resp.bodylen == 292
+       expect resp.http.X-Cache-Hit == 0
+
+       # Same two referers again -> still a miss.
+       txreq -url "/repeated-referer" \
+               -hdr "Referer: referer_a" \
+               -hdr "Referer: referer_b"
+       rxresp
+       expect resp.status == 200
+       expect resp.bodylen == 292
+       expect resp.http.X-Cache-Hit == 0
+
+       # A single referer is well-formed and must be cached normally.
+       txreq -url "/repeated-referer" \
+               -hdr "Referer: referer_a"
+       rxresp
+       expect resp.status == 200
+       expect resp.bodylen == 303
+       expect resp.http.X-Cache-Hit == 0
+
+       # Same single referer again -> cache hit on its own entry.
+       txreq -url "/repeated-referer" \
+               -hdr "Referer: referer_a"
+       rxresp
+       expect resp.status == 200
+       expect resp.bodylen == 303
+       expect resp.http.X-Cache-Hit == 1
+
        # The following requests are treated by a backend that does not cache
        # responses containing a Vary header
        txreq -url "/no_vary_support"
index 254d8a20a8ffebc6fcbdc79d43feb7d72fa2fba1..f71473921dd7636dac6509d4bfc73a293d8da4e8 100644 (file)
@@ -3370,12 +3370,13 @@ static int accept_encoding_normalizer(struct htx *htx, struct ist hdr_name,
 #undef ACCEPT_ENCODING_MAX_ENTRIES
 
 /*
- * Normalizer used by default for the Referer and Origin header. It only
- * calculates a hash of the whole value using xxhash algorithm.
- * Only the first occurrence of the header will be taken into account in the
- * hash.
+ * Normalizer used by default for the Referer and Origin header. It calculates
+ * a hash of the whole value using xxhash algorithm. Both headers are single-
+ * valued, but we can't predict how servers would handle extra occurrences,
+ * so better just ignore the secondary keys in case of extra entry, by
+ * returning -1. That way we continue to hash only the full header line.
  * Returns 0 in case of success, 1 if the hash buffer should be filled with 0s
- * and -1 in case of error.
+ * and -1 in case of error (header present more than once).
  */
 static int default_normalizer(struct htx *htx, struct ist hdr_name,
                              char *buf, unsigned int *buf_len)
@@ -3387,6 +3388,10 @@ static int default_normalizer(struct htx *htx, struct ist hdr_name,
                retval = 0;
                write_u64(buf, XXH3(istptr(ctx.value), istlen(ctx.value), cache_hash_seed));
                *buf_len = sizeof(uint64_t);
+
+               /* Make sure the header is unique and disable caching on duplicate */
+               if (http_find_header(htx, hdr_name, &ctx, 1))
+                       retval = -1;
        }
 
        return retval;
@@ -3425,8 +3430,6 @@ static int accept_encoding_bitmap_cmp(const void *ref, const void *new, unsigned
  * implementation) of a given request. We have to calculate all the hashes
  * in advance because the actual Vary signature won't be known until the first
  * response.
- * Only the first occurrence of every header will be taken into account in the
- * hash.
  * If the header is not present, the hash portion of the given header will be
  * filled with zeros.
  * Returns 0 in case of success.
@@ -3443,8 +3446,6 @@ static int http_request_prebuild_full_secondary_key(struct stream *s)
  * Calculate the secondary key for a request for which we already have a known
  * vary signature. The key is made by aggregating hashes calculated for every
  * header mentioned in the vary signature.
- * Only the first occurrence of every header will be taken into account in the
- * hash.
  * If the header is not present, the hash portion of the given header will be
  * filled with zeros.
  * Returns 0 in case of success.