Add explicit data binding API#15808
Conversation
Assisted-by: Hephaestus:openai/gpt-5.5
Require controller secureBindData calls to provide an explicit allowlist at compile time, including trait helper and inherited action paths. Document secureBindData in the controller reference and data binding guide. Assisted-by: Hephaestus:gpt-5.5
Assisted-by: opencode:gpt-5.5
Assisted-by: opencode:gpt-5.5
There was a problem hiding this comment.
Pull request overview
This PR introduces a new secureBindData controller API intended to make request data binding safer by requiring an explicit allowlist of bindable properties, and adds compiler-time validation to prevent accidental use without an allowlist. It extends the binding implementation to handle nested, indexed, map/collection, and request-body (JSON) scenarios, and documents the new API.
Changes:
- Add
secureBindDataAPIs (object + collection binding) and corresponding secure binding/filtering logic inDataBindingUtils. - Enforce controller compile-time validation: calling
secureBindDatawithout an explicitallowedParamslist becomes a compilation error. - Add extensive unit/integration tests and documentation for
secureBindData, includingnullMissing, prefix filtering, nested/indexed binding, and JSON request bodies.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java | Implements secure allowlist-filtered binding sources and nullMissing clearing logic; refactors entity lookup and binding result processing. |
| grails-web-databinding/src/main/groovy/grails/web/databinding/DataBinder.groovy | Adds secureBindData trait overloads for object and collection binding (including request-body collection binding). |
| grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy | Adds unit tests covering allowlist-only binding, prefix filtering, nested/indexed/map binding, checkbox markers, JSON bodies, and nullMissing. |
| grails-test-suite-web/src/test/groovy/org/grails/web/metaclass/CollectionBindDataMethodSpec.groovy | Adds request-body JSON collection binding test for secureBindData allowlist behavior. |
| grails-test-examples/hibernate7/app1/src/integration-test/groovy/functionaltests/binding/AdvancedDataBindingSpec.groovy | Adds functional tests demonstrating secureBindData behavior in a real app scenario. |
| grails-test-examples/hibernate7/app1/grails-app/controllers/functionaltests/binding/AdvancedDataBindingController.groovy | Adds controller endpoints exercising secureBindData for functional tests. |
| grails-test-examples/app1/src/integration-test/groovy/functionaltests/binding/AdvancedDataBindingSpec.groovy | Adds equivalent functional tests for the non-hibernate7 example app. |
| grails-test-examples/app1/grails-app/controllers/functionaltests/binding/AdvancedDataBindingController.groovy | Adds equivalent controller endpoints for the non-hibernate7 example app. |
| grails-doc/src/en/ref/Controllers/secureBindData.adoc | Introduces reference documentation for the new secureBindData API and semantics. |
| grails-doc/src/en/ref/Controllers/bindData.adoc | Adds guidance to prefer secureBindData for untrusted request parameters. |
| grails-doc/src/en/guide/theWebLayer/controllers/dataBinding.adoc | Adds user guide coverage and examples for secureBindData, including nullMissing and collections. |
| grails-doc/src/en/guide/reference.adoc | Adds secureBindData to the reference index. |
| grails-doc/src/en/guide/index.adoc | Adds secureBindData to the guide index. |
| grails-controllers/src/test/groovy/org/grails/compiler/web/ControllerActionTransformerCompilationErrorsSpec.groovy | Adds compilation tests ensuring controllers must pass an explicit allowlist to secureBindData (with exceptions for user-defined helpers). |
| grails-controllers/src/main/groovy/org/grails/compiler/web/ControllerActionTransformer.java | Adds AST validation that flags controller secureBindData calls missing an allowlist argument. |
💡 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 #15808 +/- ##
==================================================
- Coverage 49.4842% 49.2207% -0.2635%
- Complexity 16697 16715 +18
==================================================
Files 1947 1947
Lines 92474 93225 +751
Branches 16152 16362 +210
==================================================
+ Hits 45760 45886 +126
- Misses 39606 40198 +592
- Partials 7108 7141 +33
🚀 New features to boost your workflow:
|
Proves four issues found during review of the secure data binding feature with red tests before fixing them: - the compile-time allowedParams check accepts a list built from request data instead of requiring a literal - an indexed collection path beyond autoGrowCollectionLimit crashes DataBindingUtils.getIndexedValue during nullMissing processing - a non-numeric indexed bracket against a List property crashes the pre-existing SimpleDataBinder indexed-property parsing, reachable via both plain bindData and secureBindData nullMissing Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Enforce that secureBindData's allowedParams resolves to a compile-time literal list (or a reference to a static final constant list), not a dynamically built one. A method call, ternary, non-final field, or variable reassigned from request data is now rejected at compile time, closing the loophole where a controller could pass an attacker-influenced allow-list past the framework validation. - Guard DataBindingUtils.getIndexedValue/setPropertyValueToNull so an out-of-range indexed property (one the binder silently skipped for exceeding autoGrowCollectionLimit) is treated as absent instead of throwing IndexOutOfBoundsException during secureBindData's nullMissing processing. - Guard the pre-existing SimpleDataBinder.processIndexedProperty against a non-numeric bracket index on a List/array property, which previously threw an uncaught NumberFormatException and caused the generic catch in DataBindingUtils to discard the whole BindingResult, reachable via both plain bindData and secureBindData. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Pre-release review pass (2026-07-02, dual-reviewer: GPT-5.5 oracle + Codex CLI): BLOCK at the reviewed head - do not merge until resolved. (Note: the branch received new pushes during review, so some items may already be in flight.)
Positive: the distinct |
We addressed the first three, we will address the last two |
…ndings - Warn in both the guide and reference docs that allowedParams must be developer-controlled (a literal list or a static final constant), never built from params/request data, matching the compile-time enforcement. - Add secureBindData coverage proving @BindUsing and @BindingFormat properties bind correctly when allowed and stay unbound when not in the allowlist. - Rename the three now-fixed "GAP:" test titles to describe the guaranteed behavior instead of the prior defect. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
jdaugherty
left a comment
There was a problem hiding this comment.
bindData already supports whitelist and deny lists. Calling this "Secure" is a misnomer and leads to confusion. There also exists DataBindingUtils.bindObjectToInstance() which takes a whitelist. I do not agree with this naming.
| } | ||
| } else if (Collection.isAssignableFrom(propertyType)) { | ||
| def index = Integer.parseInt(indexedPropertyReferenceDescriptor.index) | ||
| Integer index = parseIndex(indexedPropertyReferenceDescriptor.index) |
There was a problem hiding this comment.
Please revert the typed variables - they can be inferred since the return types are the type being defined. This goes for all of these values.
|
@jdaugherty Since When a parameter key is absent from the request (as opposed to present but empty), Grails data binding simply does not touch the corresponding property on the target object. The existing value (or the domain/command object’s current/default value) is left unchanged. Only parameters that are actually present in the source map get bound; there is no automatic “set to null if missing” behavior. So without extra work you do not get explicit, complete-state data updates. The two combined provide OWASP best practices for data binding. This is possible with |
I am fine creating aliases that make this easier to call, but calling it secure implies the other is insecure, which is false. |
The generic exception fallback in secureBindObjectToInstance and bindObjectToDomainInstance passed e.getMessage() straight into ObjectError, which can be null and yields an empty/unhelpful binding error. Add resolveBindingErrorMessage to fall back to the exception class name when the message is null, keeping binding errors actionable. Addresses GitHub Copilot review findings on PR #15808. Assisted-by: claude-code:claude-4.8-opus
Local variable types are inferable from parseIndex's return type; keep the existing def-typed style used throughout this file. Per review from @jdaugherty on PR #15808.
… into feat/secure-bind-data
Verify that when the underlying data binder throws an exception whose message is null, both bindData and secureBindData surface a non-null ObjectError default message (the exception class name) rather than a null/empty one. The spec registers a throwing grailsWebDataBinder in its own ControllerUnitTest context so it does not affect sibling specs. Locks in the resolveBindingErrorMessage fallback added for the Copilot review findings on PR #15808. Assisted-by: claude-code:claude-4.8-opus
✅ All tests passed ✅🏷️ Commit: 3f61e1c Learn more about TestLens at testlens.app. |
The Problem
Grails
bindDatabinds whatever is in the request onto a domain or command object. Unless the developer remembers to pass a whitelist, that is an implicit mass-assignment / parameter-injection surface: an attacker can set fields the developer never intended to expose, and there is nothing at compile time that forces an allow-list to exist.The Fix
This PR adds a
secureBindDataAPI that makes the allow-list mandatory and verifies it at compile time.secureBindDatawill not bind without an explicit list of permitted parameter names, so domains and command objects are never exposed to mass assignment by accident.secureBindDatacall is validated during AST transformation (including trait-helper and inherited-action paths). The allow-list must resolve to a compile-time literal list (or a reference to astatic finalconstant list). A method call, ternary, non-final field, or a variable reassigned from request data is rejected at compile time, closing the loophole where a controller could sneak an attacker-influenced allow-list past the framework.@BindUsing/@BindingFormatproperties.nullMissing- allowed fields absent from the request can be intentionally cleared.Hardening found during review
Red regression tests were written first, then fixed:
autoGrowCollectionLimit(silently skipped by the binder) previously threwIndexOutOfBoundsExceptionduringnullMissingprocessing. Now treated as absent.items[bad]against a List/array property threw an uncaughtNumberFormatExceptionin the pre-existingSimpleDataBinder, which caused the generic catch inDataBindingUtilsto discard the wholeBindingResult. Now guarded (fixes bothbindDataandsecureBindData).e.getMessage()(which can be null) straight intoObjectError, yielding empty/unhelpful binding errors. AddedresolveBindingErrorMessageto fall back to the exception class name.Documentation
New
secureBindDatacontroller reference page, plus data-binding guide and reference coverage. Docs warn that the allow-list must be developer-controlled (a literal orstatic finalconstant), never built from request/paramsdata - matching the compile-time enforcement. Hibernate 7 example app mirrors the app1 binding examples.Testing
ControllerActionTransformerCompilationErrorsSpec(compile-time allow-list rejection),BindDataMethodTests,BindDataBinderExceptionSpec,CollectionBindDataMethodSpec,SimpleDataBinderSpec, and the app1 + hibernate7AdvancedDataBindingSpecintegration specs pass.