IT IS NOT A HASH, AND THAT IS THE DELIBERATE CHOICE. The obvious design is
hash(method + url + body), and both available hashes are wrong here:
A NON-CRYPTOGRAPHIC hash (FNV-1a, djb2) is forgeable. This key decides which cached response
a verification reads, so an attacker who can craft a body that collides with a known-good
verification gets that success served to their own submission. A captcha bypass through a
hash collision is a worse bug than the one the tier exists to fix.
A CRYPTOGRAPHIC hash cannot be computed here. crypto.subtle.digest is async, and this key
has to be derived inside the synchronous cfwQueueFetch and cfwHttpCacheGet calls that PHP
makes. Shipping a synchronous SHA-256 to avoid that is a lot of code to reintroduce a
collision domain that does not have to exist.
So the key is the exact tuple, LENGTH-PREFIXED. Collisions are impossible by construction rather
than improbable, the derivation is trivially synchronous, and the cost is index size -- bounded by
MAX_DEFERRED_BODY.
The length prefix is the security property, and a separator is not good enough. The first
version joined the fields with a NUL, reasoning that a NUL cannot appear in a method or a URL. It
can appear in a BODY, and a body is attacker-controlled: two different (url, body) pairs can be
made to serialise identically by moving the separator between them. That is the
forgeable-collision hole this function exists to close, reintroduced by its own encoding, and the
spec case named for it is what caught it.
Length prefixes make the encoding injective for ANY field contents, because nothing has to guess
where a field ends.
The cache key for a deferred request.
IT IS NOT A HASH, AND THAT IS THE DELIBERATE CHOICE. The obvious design is
hash(method + url + body), and both available hashes are wrong here:crypto.subtle.digestis async, and this key has to be derived inside the synchronouscfwQueueFetchandcfwHttpCacheGetcalls that PHP makes. Shipping a synchronous SHA-256 to avoid that is a lot of code to reintroduce a collision domain that does not have to exist.So the key is the exact tuple, LENGTH-PREFIXED. Collisions are impossible by construction rather than improbable, the derivation is trivially synchronous, and the cost is index size -- bounded by
MAX_DEFERRED_BODY.The length prefix is the security property, and a separator is not good enough. The first version joined the fields with a NUL, reasoning that a NUL cannot appear in a method or a URL. It can appear in a BODY, and a body is attacker-controlled: two different (url, body) pairs can be made to serialise identically by moving the separator between them. That is the forgeable-collision hole this function exists to close, reintroduced by its own encoding, and the spec case named for it is what caught it.
Length prefixes make the encoding injective for ANY field contents, because nothing has to guess where a field ends.