Harden synchronizer token consumption#15803
Conversation
Bound the per-session synchronizer token holder by URL and per-URL token count, and consume withForm tokens through a single holder-level atomic validate-and-reset operation. This prevents unbounded token growth and closes the race where concurrent submits could validate the same token before reset. Assisted-by: opencode:gpt-5.5
There was a problem hiding this comment.
Pull request overview
This PR strengthens Grails’ synchronizer-token (double-submit) mechanism by bounding per-session token storage and making withForm token consumption a single atomic validate-and-reset operation, with expanded regression coverage across unit and example app tests.
Changes:
- Add bounded token storage by URL count and tokens-per-URL, and introduce an atomic
isValidAndResetToken()operation inSynchronizerTokensHolder. - Update
Controller.withForm(...)to consume tokens via the new holder-level atomic operation and remove the session holder when empty. - Add/extend regression tests for bounded storage and one-time token consumption behavior (including concurrent consumption).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| grails-web-mvc/src/main/groovy/org/grails/web/servlet/mvc/SynchronizerTokensHolder.groovy | Adds synchronized validation/consumption, bounded storage, and ordered eviction of URLs/tokens. |
| grails-test-suite-web/src/test/groovy/org/grails/web/servlet/mvc/SynchronizerTokensHolderTests.groovy | Adds unit tests for bounds, one-time consumption, malformed tokens, sibling tokens, and concurrent consume. |
| grails-test-examples/hibernate7/demo33/src/test/groovy/demo/TestControllerSpec.groovy | Extends example spec to assert withForm reuse/double-submit results in invalid token handling. |
| grails-test-examples/demo33/src/test/groovy/demo/TestControllerSpec.groovy | Same as above for the non-hibernate7 demo example. |
| grails-controllers/src/main/groovy/grails/artefact/Controller.groovy | Switches withForm to consume-and-reset atomically via the holder, and clears the holder from session when empty. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15803 +/- ##
==================================================
+ Coverage 49.4679% 49.4886% +0.0207%
- Complexity 16684 16702 +18
==================================================
Files 1947 1947
Lines 92468 92500 +32
Branches 16150 16158 +8
==================================================
+ Hits 45742 45777 +35
+ Misses 39620 39611 -9
- Partials 7106 7112 +6
🚀 New features to boost your workflow:
|
jdaugherty
left a comment
There was a problem hiding this comment.
I'm missing something - aren't session tokens supposed to be valid for all URLs?
|
This PR doesn't adhere to our PR summary guidelines. Please update it. |
|
Thanks. I updated the PR body to follow the summary policy more explicitly: it now separates "What was found", "What was fixed", and "Verification". On URL scope: this PR preserves the existing holder contract, which already stores and validates tokens by URL ( |
Ensure existing oversized synchronizer token holders are reduced to the configured URL and per-URL token limits in a single token generation. Cover upgraded-session states where older unbounded holders may already exceed the new limits. Assisted-by: Hephaestus:openai/gpt-5.5
|
Maintenance pass complete. The two Copilot threads were already replied to and marked resolved, and I confirmed that status through the review-thread API. I reran the focused SynchronizerTokensHolderTests and WithFormMethodTests successfully. I also documented the Codecov/TestLens bot feedback in the PR body: Codecov's low patch coverage is covered by focused tests, and TestLens flagged unrelated scaffolding test flakiness while reporting checks completed. |
Document single-use atomic token consumption for withForm and the bounded session token storage (100 URL buckets, 100 tokens per URL) introduced by the synchronizer token hardening, in the form tokens guide and the withForm reference page. Assisted-by: opencode:gpt-5.5
|
Pre-release review pass (2026-07-02, dual-reviewer: GPT-5.5 oracle + Codex CLI): NEEDS-IMPROVEMENT, now addressed. Findings: the new bounded token retention (100 URL buckets / 100 tokens per URL, oldest trimmed) is user-visible but was undocumented (med) - fixed in |
✅ All tests passed ✅🏷️ Commit: 5130d3c Learn more about TestLens at testlens.app. |
Can you fix the formatting on this? |
The Problem
The synchronizer-token holder (backing
withForm) kept token state in the HTTP session with no per-session bounds, andwithFormvalidation reset token state as a separate step from validating it. That left two hardening gaps:Copilot also flagged an upgrade edge case: if a holder was already oversized, single-entry eviction would leave it above the new cap for many future token generations.
The Fix
Bound the storage and make validate-and-reset a single atomic operation.
withFormthrough it, so a valid token cannot be accepted twice under concurrency.generateToken(url),isValid(url, token),resetToken(url, token), and theTOKEN_URIrequest parameter are unchanged. Tokens are not made more URL-specific than they already were; the existing URL-keyed storage is simply bounded.Files
SynchronizerTokensHolder.groovy- bounds, atomic consume, oversized-state trimming.Controller.groovy-withFormuses the atomic consume.SynchronizerTokensHolderTests.groovy- bounds, atomicity, trim, and malformed-payload coverage.formtokens.adoc,withForm.adoc- document single-use atomic consumption and the session bounds.Testing
SynchronizerTokensHolderTests,WithFormMethodTests, and the demo33 + hibernate7/demo33test form token synchronizationexample specs pass;:grails-web-mvc:checkclean.Open discussion
Whether tokens should be URL-scoped at all is an existing-contract question left open for maintainers. A rare pre-existing race (a concurrent
<g:form useToken>render racing empty-holder session removal after a consume) predates this PR and is unchanged by it.