Harden bindData against mass assignment#15947
Conversation
Default data binding now denies properties unless they are explicitly marked bindable or named by @BindAllowed on action parameters. The legacy grails.databinding.legacyBindableDefault flag keeps old opt-out binding behavior while preserving bindable:false and domain special-property exclusions. Assisted-by: Sisyphus-Junior:openai/gpt-5.5 codex
There was a problem hiding this comment.
Pull request overview
This PR hardens Grails’ mass data binding to mitigate mass-assignment (CWE-915) by switching to an explicit allowlist model for bindData defaults and controller action auto-binding, while providing a legacy opt-out for migration.
Changes:
- Default binding behavior becomes deny-by-default unless properties are explicitly opted in (primarily via
bindable: trueallowlists generated at compile time). - Introduces
@BindAllowed([...])for controller action parameters to provide action-specific binding allowlists. - Adds a migration switch (
grails.databinding.legacyBindableDefault=true), updates documentation, and expands test coverage for the new defaults and edge cases (including emptyinclude).
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| grails-web-databinding/src/main/groovy/org/grails/web/databinding/DefaultASTDatabindingHelper.java | Generates both default and legacy binding allowlist fields during AST injection. |
| grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java | Enforces deny-by-default include behavior and adds legacy-default config support. |
| grails-web-databinding/src/main/groovy/grails/web/databinding/DataBinder.groovy | Treats explicit empty include as “bind nothing” to match new semantics. |
| grails-web-databinding/src/main/groovy/grails/web/databinding/BindAllowed.java | Adds @BindAllowed annotation for controller action parameter allowlists. |
| grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy | Updates and extends bindData tests for deny-by-default and empty-include behavior. |
| grails-test-suite-web/src/test/groovy/org/grails/web/commandobjects/SomeValidateableClass.groovy | Updates constraints to explicitly opt properties into binding. |
| grails-test-suite-web/src/test/groovy/org/grails/web/commandobjects/NonValidateableCommand.groovy | Adds constraints to opt properties into binding under the new default model. |
| grails-test-suite-web/src/test/groovy/org/grails/web/commandobjects/CommandObjectsSpec.groovy | Adds @BindAllowed coverage and updates command-object constraints for new defaults. |
| grails-test-suite-web/src/test/groovy/org/grails/web/binding/DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec.groovy | Expands coverage for default deny behavior and legacy flag behavior in domain inheritance/special properties. |
| grails-doc/src/en/ref/Controllers/bindData.adoc | Documents new default allowlist behavior, empty-include semantics, and @BindAllowed. |
| grails-doc/src/en/ref/Constraints/bindable.adoc | Updates bindable semantics to reflect explicit opt-in model and legacy migration option. |
| grails-doc/src/en/guide/upgrading.adoc | Adds upgrading notes for the new data binding defaults and migration flag. |
| grails-doc/src/en/guide/theWebLayer/controllers/dataBinding.adoc | Updates guide note for empty include behavior and documents allowlist options. |
| grails-controllers/src/main/groovy/org/grails/compiler/web/ControllerActionTransformer.java | Threads @BindAllowed allowlists into command object initialization during action parameter binding. |
| grails-controllers/src/main/groovy/grails/artefact/Controller.groovy | Adds an overload of initializeCommandObject that accepts allowlisted bind properties and applies them during binding. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| includeList = includeListCache.get(objectClass); | ||
| } else { | ||
| final Field whiteListField = objectClass.getDeclaredField(DefaultASTDatabindingHelper.DEFAULT_DATABINDING_WHITELIST); | ||
| final Field whiteListField = objectClass.getDeclaredField(whiteListFieldName); |
|
The issue described regarding |
🚨 TestLens detected 3840 failed tests 🚨Here is what you can do:
Test SummaryCI - Groovy Joint Validation Build / build_grails > :grails-databinding:test
CI - Groovy Joint Validation Build / build_grails > :grails-fields:test (first 40 of 139)
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-graphql-grails-tenant-app:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-graphql-grails-test-app:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate5-grails-database-per-tenant:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate5-grails-hibernate:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate5-grails-multiple-datasources:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate5-grails-partitioned-multi-tenancy:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate5-grails-schema-per-tenant:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-app1:integrationTest (first 40 of 712)
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-database-cleanup:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-datasources:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-gorm:integrationTest (first 40 of 161)
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-grails-database-per-tenant:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-grails-hibernate:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-grails-multiple-datasources:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-grails-partitioned-multi-tenancy:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-grails-schema-per-tenant:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-issue-698-domain-save-npe:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-scaffolding-fields:integrationTest (first 40 of 105)
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-hibernate7-views-functional-tests:integrationTest (first 40 of 53)
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-issue-15228:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-issue-698-domain-save-npe:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-mongodb-base:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-mongodb-hibernate5:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-redis:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-scaffolding-fields:integrationTest (first 40 of 105)
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-scaffolding:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-spring-security-acl-functional-test-app:integrationTest (first 40 of 285)
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-spring-security-acl-integration-test-app:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-spring-security-core-integration-test-app:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-spring-security-core-misc-functional-test-app-group:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-spring-security-core-misc-functional-test-app-roles:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-spring-security-ui-extended:integrationTest (first 40 of 49)
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-spring-security-ui-simple:integrationTest
CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-views-functional-tests:integrationTest (first 40 of 53)
CI / Build Grails-Core (macos-latest, 21) > :grails-test-suite-persistence:test (first 40 of 73)
CI / Build Grails-Core (macos-latest, 21) > :grails-test-suite-uber:test
CI / Build Grails-Core (macos-latest, 21) > :grails-test-suite-web:test (first 40 of 93)
CI / Build Grails-Core (macos-latest, 21) > :grails-views-gson:test
CI / Build Grails-Core (ubuntu-latest, 21) > :grails-rest-transforms:test
CI / Build Grails-Core (ubuntu-latest, 21) > :grails-test-suite-persistence:test (first 40 of 73)
CI / Build Grails-Core (ubuntu-latest, 21) > :grails-test-suite-uber:test
CI / Build Grails-Core (ubuntu-latest, 21) > :grails-test-suite-web:test (first 40 of 93)
CI / Build Grails-Core (ubuntu-latest, 21) > :grails-views-gson:test (first 40 of 51)
CI / Build Grails-Core (ubuntu-latest, 25) > :grails-rest-transforms:test
CI / Build Grails-Core (ubuntu-latest, 25) > :grails-test-suite-persistence:test (first 40 of 73)
CI / Build Grails-Core (ubuntu-latest, 25) > :grails-test-suite-uber:test
CI / Build Grails-Core (ubuntu-latest, 25) > :grails-test-suite-web:test (first 40 of 93)
CI / Build Grails-Core (ubuntu-latest, 25) > :grails-views-gson:test
CI / Build Grails-Core (windows-latest, 25) > :grails-databinding:test
CI / Build Grails-Core (windows-latest, 25) > :grails-fields:test (first 40 of 139)
CI / Build Grails-Core (windows-latest, 25) > :grails-web-url-mappings:test
CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) > :grails-fields:test (first 40 of 139)
CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) > :grails-rest-transforms:test
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-app1:integrationTest (first 40 of 714)
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-app1:test
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-database-cleanup:integrationTest
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-datasources:integrationTest
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-demo33:test
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-gorm:integrationTest (first 40 of 162)
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-graphql-grails-docs-app:integrationTest
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-graphql-grails-multi-datastore-app:integrationTest
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-graphql-grails-tenant-app:integrationTest
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-graphql-grails-test-app:integrationTest
CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-views-functional-tests:test
CI / Functional Tests (Java 21, indy=true) > :grails-test-examples-app1:test
CI / Functional Tests (Java 21, indy=true) > :grails-test-examples-demo33:test
CI / Functional Tests (Java 25, indy=false) > :grails-test-examples-app1:test
CI / Functional Tests (Java 25, indy=false) > :grails-test-examples-demo33:test
CI / Functional Tests (Java 25, indy=false) > :grails-test-examples-views-functional-tests:test
CI / Redis Tests (21, 7.4) > :grails-test-suite-uber:test
CI / Redis Tests (21, 8.0) > :grails-test-suite-uber:test
CI / Redis Tests (25, 7.4) > :grails-test-suite-uber:test
CI / Redis Tests (25, 8.0) > :grails-test-suite-uber:test
CI / Spring Security Tests (21) > :grails-test-examples-spring-security-ui-extended:test
CI / Spring Security Tests (25) > :grails-test-examples-spring-security-ui-extended:test
🏷️ Commit: e2926e0 Test Failures (first 10 of 3840)BindCommandObjectsSpec > Test bind command to domain with constraints (:grails-test-suite-web:test in CI / Build Grails-Core (macos-latest, 21))
BindCommandObjectsSpec > Test bind domain to command (:grails-test-suite-web:test in CI / Build Grails-Core (macos-latest, 21))BindStringArrayToGenericListTests > testBindStringArrayToGenericList (:grails-test-suite-web:test in CI / Build Grails-Core (macos-latest, 21))
BindToEnumTests > testBindValueToEnum (:grails-test-suite-web:test in CI / Build Grails-Core (macos-latest, 21))
BindToObjectWithEmbeddableTests > testBindToObjectWithEmbedded (:grails-test-suite-web:test in CI / Build Grails-Core (macos-latest, 21))
BindToPropertyThatIsNotReadableTests > testBindToPropertyThatIsNotReadable (:grails-test-suite-web:test in CI / Build Grails-Core (macos-latest, 21))BindXmlWithAssociationTests > testBindXmlWithAssociatedIdAndProperties (:grails-test-suite-web:test in CI / Build Grails-Core (macos-latest, 21))
BindingExcludeTests > testBindingExcludeExpressedAsGstringExclude (:grails-test-suite-web:test in CI / Build Grails-Core (macos-latest, 21))
BindingExcludeTests > testThatAssociationsAreExcluded (:grails-test-suite-web:test in CI / Build Grails-Core (macos-latest, 21))
BindingRequestMethodSpec > Test binding to a domain class command object (:grails-test-suite-web:test in CI / Build Grails-Core (macos-latest, 21))
Muted Tests (first 20 of 3840)Select tests to mute in this pull request:
Reuse successful test results:
Click the checkbox to trigger a rerun:
Learn more about TestLens at testlens.app. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15947 +/- ##
================================================
+ Coverage 0 19.5796% +19.5796%
- Complexity 0 308 +308
================================================
Files 0 61 +61
Lines 0 3473 +3473
Branches 0 602 +602
================================================
+ Hits 0 680 +680
- Misses 0 2663 +2663
- Partials 0 130 +130 🚀 New features to boost your workflow:
|
Summary
Hardens existing
bindDataand controller action auto-binding against mass assignment (OWASP / CWE-915). Does not add a newsecureBindDatamethod.Changes
bindable: true@BindAllowed([...])annotation on controller action parameters for action-specific allowlists@BindAllowedviainitializeCommandObject(..., allowedList)grails.databinding.legacyBindableDefault=truerestores prior "all static properties bindable" behaviorbindable,bindData, data binding guide, and upgrading notesExplicitly out of scope (follow-up PR)
nullMissing) - will be a separate PRsecureBindData)Migration
bindable: truein constraints, orbindData(target, source, [include: [...]]), ordef update(@BindAllowed(['title','author.id']) Book book), orgrails.databinding.legacyBindableDefault=trueRelated
bindData, do not invent a "secure" alias)Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]