From: Willy Tarreau Date: Sun, 16 Aug 2026 06:09:32 +0000 (+0200) Subject: BUG/MEDIUM: cache: ignore cache on redundant origin/referer X-Git-Tag: v3.5-dev6~71 X-Git-Url: https://git.kaiwu.me/?a=commitdiff_plain;h=34dbc5e69fb1f59d4fc0614e8402c12cf489b595;p=haproxy.git BUG/MEDIUM: cache: ignore cache on redundant origin/referer 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. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index 89a088511..5a5dcf9c1 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -20461,7 +20461,11 @@ process-vary (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 Define the size in RAM of the cache in megabytes. This size is split in diff --git a/reg-tests/cache/vary.vtc b/reg-tests/cache/vary.vtc index 5a6e6986c..4f34a1222 100644 --- a/reg-tests/cache/vary.vtc +++ b/reg-tests/cache/vary.vtc @@ -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 == "" 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" diff --git a/src/cache.c b/src/cache.c index 254d8a20a..f71473921 100644 --- a/src/cache.c +++ b/src/cache.c @@ -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.