Repository navigation
v2.32.0 — the last thing the client chose
v2.32.0
The last thing the client chose.
2.30.0 put a ceiling on how long a challenge pass could last, because the lifetime that signs the pass token arrived in the interstitial's POST body and nothing bounded it. This removes the proposal rather than bounding it.
A one-item release. The other item in this milestone turned out to need a design decision rather than a fix, and moved to 3.x — see below.
PHP 8.1–8.5 supported. No behaviour changes on upgrade, and no new configuration.
Changed
-
The pass lifetime is carried signed, so the client never proposes it (#369)
challenge_provider = math|900.k8Hs3... ← signed; this is what decides ttl = 900 ← still rendered, no longer believedIt could not simply be deleted. The submission arrives at
challenge.path, not at the protected URL, so by then nothing in the request says which rule sent the visitor andmetadata.default_expiration_timeis not knowable. That is why the value had to travel with them, and why the fix was never "stop reading it".It travels in the signed
provider_tokennow — a field that already existed for exactly this class of problem, carrying the provider name since 2.26.0 so amathpass cannot satisfy a reCAPTCHA rule.The resolved TTL, not the rule's identity.
metadata.nameis optional and theRateLimit:2fallback moves the moment somebody reordersplugins:, so carrying identity would make a stable signature depend on an unstable value. The submission handler does not need to know which rule matched, only how long the pass should last.Still clamped, not trusted outright.
challenge.ttlmay have been lowered while the page was open, and a ceiling that applied only to values the client sent would be a ceiling with a gap in it.
Upgrading mid-challenge is safe
An interstitial rendered by 2.31.0 or earlier carries the provider name alone. Those are accepted for one minor and fall back to the posted value clamped by 2.30.0's ceiling — which is exactly the defence in depth that ceiling was written to be, and the reason it shipped a release earlier rather than being folded into this one.
Without that fallback, every visitor who happened to be mid-solve when the deploy landed would be refused. The same path covers a custom provider that builds provider_token itself rather than echoing the one it was handed.
A refused submission is re-rendered with a signed lifetime too, so a visitor who fails once does not drop back to proposing their own.
Nothing you rely on moved
ChallengeRequiredException::getRenderContext()['ttl'] is public API — hosts rendering their own page in mode: exception read it — and it means what it meant. The form keeps posting ttl, because custom providers are documented to echo it and removing it would break them for no gain once it is ignored.
Moved to 3.x: the challenged POST
#376 — a visitor whose POST is challenged solves the interstitial and lands on a GET with their submission gone — was the other half of this milestone. A fix is written and deliberately unmerged, because the advice it was built around did not survive contact with the evidence.
"Challenge the GET that renders the form, not the POST that submits it" was going to be the answer. It is not sufficient:
A. pass expires mid-form
immediately: ALLOWED
2s later (ttl=1s): CHALLENGED — submission discarded
B. address changes mid-form
same address: ALLOWED
different address: CHALLENGED — submission discarded
A fails on exactly the submissions worth most, because taking longer on a form is what expires the pass. B is not time-dependent at all — it is every visitor whose phone moves from wifi to cellular mid-form.
Vendor behaviour settled the direction. AWS WAF refuses to challenge non-GET requests at all and recommends acquiring the token client-side before the POST. Cloudflare preserves POSTs with a 307, which it can do because it is a reverse proxy replaying origin-ward — and that is documented as unreliable. This library runs inside the origin, so the proxy mechanism is not available to it; the only way to produce a second POST from here is to put the data back in the browser.
The real question is whether a rule can be satisfied by a challenge token the application's own form already carries. That is a change to how a rule is satisfied, which belongs with the evaluation model 3.x is already reconsidering rather than bolted beside the interstitial in a minor.
The reasoning, the options table and the sources are all on #376. Nothing about the current behaviour changed in this release.
Upgrading
$ composer require kanopi/firewall:^2.32Nothing to configure and nothing to check. If you have visitors mid-challenge when this deploys, they are covered by the fallback above.
Full changelog: v2.31.0...v2.32.0