You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/07/02 00:26:35 UTC
(grails-core) branch feat/secure-bind-data updated: Add failing regression tests for secureBindData review findings
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch feat/secure-bind-data
in repository https://gitbox.apache.org/repos/asf/grails-core.git
The following commit(s) were added to refs/heads/feat/secure-bind-data by this push:
new 755daa848f Add failing regression tests for secureBindData review findings
755daa848f is described below
commit 755daa848f0a324a0cf9034708badaddebd54b4a
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Wed Jul 1 19:26:17 2026 -0500
Add failing regression tests for secureBindData review findings
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 <no...@anthropic.com>
---
...erActionTransformerCompilationErrorsSpec.groovy | 21 ++++++++++
.../grails/web/servlet/BindDataMethodTests.groovy | 45 ++++++++++++++++++++++
2 files changed, 66 insertions(+)
diff --git a/grails-controllers/src/test/groovy/org/grails/compiler/web/ControllerActionTransformerCompilationErrorsSpec.groovy b/grails-controllers/src/test/groovy/org/grails/compiler/web/ControllerActionTransformerCompilationErrorsSpec.groovy
index 397ff29386..235964fa95 100644
--- a/grails-controllers/src/test/groovy/org/grails/compiler/web/ControllerActionTransformerCompilationErrorsSpec.groovy
+++ b/grails-controllers/src/test/groovy/org/grails/compiler/web/ControllerActionTransformerCompilationErrorsSpec.groovy
@@ -487,4 +487,25 @@ class ControllerActionTransformerCompilationErrorsSpec extends Specification {
MultipleCompilationErrorsException e = thrown()
e.message.contains 'secureBindData requires an explicit allowedParams list. Use secureBindData(target, params, allowedParams).'
}
+
+ void 'GAP: secureBindData does not reject an allowedParams list built from request data'() {
+ when: 'A controller builds its allowedParams list from request-controlled data rather than a literal'
+ gcl.parseClass('''
+ class TaintedAllowedParamsController {
+ def save() {
+ def target = new Person()
+ List allowedParams = params.fields?.tokenize(',')
+ secureBindData(target, params, allowedParams)
+ }
+ }
+
+ class Person {
+ String name
+ }
+ ''')
+
+ then: 'the compiler should reject an allowedParams list that is not a compile-time literal, since callers can smuggle attacker-controlled field names past the framework validation otherwise'
+ MultipleCompilationErrorsException e = thrown()
+ e.message.contains 'secureBindData requires allowedParams to be a literal list of property names, or a reference to a constant list. A dynamically built list can be attacker-controlled and defeats the purpose of secureBindData.'
+ }
}
diff --git a/grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy b/grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy
index c330188ca7..e3cd17f3c9 100644
--- a/grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy
+++ b/grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy
@@ -87,6 +87,19 @@ class BindDataMethodTests extends Specification implements ControllerUnitTest<Bi
target.email == 'lee@mail.com'
}
+ void 'GAP: bindData does not crash on a non-numeric indexed parameter for a List property'() {
+ when: 'a crafted request uses a non-numeric bracket index against a plain bindData call (no secureBindData involved)'
+ params.'members[abc].name' = 'Hacked'
+ def model = controller.bindWithNonNumericIndex()
+ def target = model.target
+
+ then: 'the malformed index should be ignored rather than blowing up the whole binding result'
+ model.bindingResult == null || !model.bindingResult.hasErrors()
+ target.members.size() == 1
+ target.members[0].name == 'Existing'
+ target.members[0].role == 'Existing'
+ }
+
void 'Test bindData With Disallowed And GrailsParameterMap'() {
when:
params.name = 'Marc Palmer'
@@ -426,6 +439,32 @@ class BindDataMethodTests extends Specification implements ControllerUnitTest<Bi
model.bindingResult == null || !model.bindingResult.hasErrors()
}
+ void 'GAP: secureBindData nullMissing does not crash on a non-numeric indexed parameter root'() {
+ when: 'a crafted request uses a non-numeric bracket index against a List-typed allowed property'
+ params.'members[abc].role' = 'Lead'
+ def model = controller.secureBindWithIndexedParamsAndNullMissing()
+ def target = model.target
+
+ then: 'the malformed index should be ignored rather than blowing up the whole binding result'
+ model.bindingResult == null || !model.bindingResult.hasErrors()
+ target.members.size() == 1
+ target.members[0].name == 'Existing'
+ target.members[0].role == 'Existing'
+ }
+
+ void 'GAP: secureBindData nullMissing does not crash on an out-of-range indexed parameter root'() {
+ when: 'a crafted request uses an indexed root beyond the auto-grow limit, which the binder silently ignores but nullMissing still tries to null out'
+ params.'members[300].name' = 'Alice'
+ def model = controller.secureBindWithIndexedParamsAndNullMissing()
+ def target = model.target
+
+ then: 'the out-of-range index should be ignored rather than blowing up the whole binding result'
+ model.bindingResult == null || !model.bindingResult.hasErrors()
+ target.members.size() == 1
+ target.members[0].name == 'Existing'
+ target.members[0].role == 'Existing'
+ }
+
void 'Test secureBindData nullMissing supports JSON collection roots'() {
given:
request.method = 'POST'
@@ -489,6 +528,12 @@ class BindingController {
[target: target]
}
+ def bindWithNonNumericIndex() {
+ def target = new CommandObject(members: [new Member(name: 'Existing', role: 'Existing')])
+ def bindingResult = bindData(target, params)
+ [target: target, bindingResult: bindingResult]
+ }
+
def bindWithPrefixFilterAndDisallowed() {
def target = new CommandObject()
def filter = "lee"