Fix single-use CSRF token: a failed submit no longer burns it - #105
Merged
Conversation
Zend_Form_Element_Hash arms the CSRF token with a 1-HOP session expiration, so the
first submit consumes it. If that submit fails any OTHER field's validation, the
corrected resubmit dies with "your security token expired" and the only cure is a
full page refresh — reproduced on the profile Security / change-password form (fix
the flagged New Password, resubmit, token already gone).
That contradicts Tiger_Form's own documented CSRF design ("the token isn't
single-use; it validates until it times out", CSRF_TIMEOUT = 2h, salt-shared across
related endpoints). Add Tiger_Form_Element_Hash, which arms the token on the seconds
TTL only and drops the hop limit; Tiger_Form uses it in place of the stock 'hash'.
One rendered token now survives a failed-then-corrected resubmit within the session.
Verified end-to-end on dev-com: two changePassword submits with the same rendered
token both return the field error, neither the CSRF error. Unit test asserts the
element arms on timeout (no hop), with a contrast test pinning the stock Zend default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WebTigers
added a commit
that referenced
this pull request
Aug 7, 2026
…on UX (#107) Bug-fix release: the single-use CSRF token (#105) and the change-password form's validation surfacing — localized inline field errors, the strength meter on the profile page, a clear 'Passwords do not match', and a client-side match check (#106). Also backfills the 0.51.0-beta CHANGELOG entry that was skipped. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
On the profile Security / change-password form: submit with any field invalid → "Please correct the highlighted fields" (correct). Fix the field, resubmit → "Oops — your security token expired. Please refresh the page to continue." The only cure is a full page refresh — you can't correct a validation error and resubmit.
Root cause
Zend_Form_Element_Hash::initCsrfToken()arms the token withsetExpirationHops(1, …)— a single-request expiration. The first submit consumes the token, so a submit that fails any other field's validation leaves the corrected resubmit with a spent token → CSRF failure.That directly contradicts
Tiger_Form's own documented design (csrfSalt()): "the token isn't single-use; it validates until it times out" (CSRF_TIMEOUT = 2h, salt-shared across related endpoints).The fix
Add
Tiger_Form_Element_Hash(alongside the existingTiger_Form_Element_Recaptcha) that overridesinitCsrfToken()to arm the token on the seconds TTL only — no hop limit.Tiger_Form::init()uses it in place of the stock'hash'. One rendered token now survives a failed-then-corrected resubmit within the session, exactly as the design intended. Everything else (salt, session name, hash generation, the CSRF validator) is inherited unchanged.Security posture is unchanged from the documented intent: session-lifetime CSRF tokens (not single-use) are the standard, and the salt-per-form scoping is untouched.
Verified
tests/Unit/Form/HashElementTest.php): the element arms on timeout with no hop; a contrast test pins the stock Zend 1-hop default.changePasswordsubmits with the same rendered token both return the field error — neither the CSRF error (before the fix, the 2nd was "token expired").🤖 Generated with Claude Code