Skip to content

Harden bindData against mass assignment#15947

Open
jamesfredley wants to merge 2 commits into
8.0.xfrom
fix/binddata-mass-assignment
Open

Harden bindData against mass assignment#15947
jamesfredley wants to merge 2 commits into
8.0.xfrom
fix/binddata-mass-assignment

Conversation

@jamesfredley

@jamesfredley jamesfredley commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

What was found

Problem Impact
Static domain/command properties are bindable by default Mass assignment risk (OWASP / CWE-915)
Auto-binding of typed action params inherits that default Controller actions bind unexpected fields
#15808 proposed secureBindData Maintainer feedback: harden existing bindData instead
First CI run with deny-by-default broke ~3840 tests Scaffolded apps assume legacy bind-all

What changed

Area Change
Secure mode Opt-in via grails.databinding.denyByDefault=true
8.0 default Legacy bind-all remains default so apps/tests keep working
Action allowlist @BindAllowed([...]) on controller action parameters
Class constraints bindable: true/false still honored; false always wins
Whitelist lookup Walk declared-field hierarchy for proxied subclasses
Docs / tests Secure mode, legacy default, allowlist coverage

Explicitly out of scope

Topic Where
Stale-data / nullMissing #15950
Flipping secure mode on by default Candidate for 9.0 after migration

Migration

Approach Example
Enable secure default grails.databinding.denyByDefault: true
Class-level opt-in fields static constraints = { title bindable: true }
Explicit include bindData book, params, [include: ['title']]
Action allowlist def update(@BindAllowed(['title']) Book book)

Related

PR Relationship
#15808 Prior secureBindData approach; close once this lands
#15950 Opt-in nullMissing / stale-data clearing

Contributor Checklist

Issue and Scope

  • Background explains mass-assignment hardening for Grails 8.
  • Scope is bindData / auto-binding only.
  • Single focused security change.
  • Targets 8.0.x.

Code Quality

  • Tests updated for secure + legacy modes.
  • Focused binding tests run locally.
  • No mass reformatting.
  • AI starting point labeled.

Licensing and Attribution

  • Apache License 2.0.
  • Contributor rights confirmed.
  • ai-generated-starting-point label applied.

Documentation

  • User-facing binding behavior documented.
  • PR description explains what changed and why.

Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: true allowlists 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 empty include).

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);
@bito-code-review

Copy link
Copy Markdown

The issue described regarding Class.getDeclaredField(...) failing for proxied subclasses is a valid concern in the context of Grails data binding. Using Class.getField(...) or walking the class hierarchy is the correct approach to ensure inherited public static whitelist fields are properly resolved for proxied instances. Since the provided PR context does not include the DataBindingUtils.java file, I cannot provide a direct code update for that specific file, but the proposed logic change is technically sound for resolving inherited fields.

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 19.5852%. Comparing base (05f71e6) to head (6c85e2b).
⚠️ Report is 134 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@               Coverage Diff                @@
##             8.0.x     #15947         +/-   ##
================================================
+ Coverage         0   19.5852%   +19.5852%     
- Complexity       0        308        +308     
================================================
  Files            0         61         +61     
  Lines            0       3472       +3472     
  Branches         0        601        +601     
================================================
+ Hits             0        680        +680     
- Misses           0       2662       +2662     
- Partials         0        130        +130     

see 61 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Keep legacy bind-all as the default so scaffolded apps and existing
tests keep working. Secure mode is enabled with
grails.databinding.denyByDefault=true. @BindAllowed and bindable:false
remain available. Walk declared-field hierarchy for whitelist lookup.

Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]
@testlens-app

testlens-app Bot commented Jul 10, 2026

Copy link
Copy Markdown

🚨 TestLens detected 9 failed tests 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

CI - Groovy Joint Validation Build / build_grails > :grails-databinding:test

Test Runs Flakiness
DataBindingConfigurationSpec > test custom ValueConverter are ordered if defined with @Order 0% 🟢
DataBindingConfigurationSpec > test customize data binding for the types which have standard ValueConverters using @Order 0% 🟢

CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-spring-security-core-integration-test-app:integrationTest

Test Runs Flakiness
SpringSecurityServiceIntegrationSpec > update role 0% 🟢
SpringSecurityServiceIntegrationSpec > update role when invalid 0% 🟢

CI - Groovy Joint Validation Build / build_grails > :grails-test-examples-spring-security-core-misc-functional-test-app-group:integrationTest

Test Runs Flakiness
SecuredControllerSpec > test login as sherlock, sherlock belongs to detective groups. All detectives have the role ADMIN 0% 🟢

CI / Build Grails-Core (windows-latest, 25) > :grails-databinding:test

Test Runs Flakiness
DataBindingConfigurationSpec > test custom ValueConverter are ordered if defined with @Order 0% 🟢
DataBindingConfigurationSpec > test customize data binding for the types which have standard ValueConverters using @Order 0% 🟢

CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) > :grails-databinding:test

Test Runs Flakiness
DataBindingConfigurationSpec > test custom ValueConverter are ordered if defined with @Order 0% 🟢
DataBindingConfigurationSpec > test customize data binding for the types which have standard ValueConverters using @Order 0% 🟢

🏷️ Commit: 6c85e2b
▶️ Tests: 10878 executed
⚪️ Checks: 59/59 completed

Test Failures

DataBindingConfigurationSpec > test custom ValueConverter are ordered if defined with @Order (:grails-databinding:test in CI / Build Grails-Core (windows-latest, 25))
Condition failed with Exception:

person.prop.value == "test2"
|      |    |
|      null java.lang.NullPointerException: Cannot get property 'value' on null object
|           	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test custom ValueConverter are ordered if defined with @Order(DataBindingConfigurationSpec.groovy:59)
<org.grails.plugins.databinding.DataBindingConfigurationSpec$Person@a547191 name=inaccessible dob=inaccessible prop=inaccessible>

	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test custom ValueConverter are ordered if defined with @Order(DataBindingConfigurationSpec.groovy:59)
Caused by: java.lang.NullPointerException: Cannot get property 'value' on null object
	... 1 more
DataBindingConfigurationSpec > test customize data binding for the types which have standard ValueConverters using @Order (:grails-databinding:test in CI / Build Grails-Core (windows-latest, 25))
Condition failed with Exception:

person.prop.value == "test2"
|      |    |
|      null java.lang.NullPointerException: Cannot get property 'value' on null object
|           	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test customize data binding for the types which have standard ValueConverters using @Order(DataBindingConfigurationSpec.groovy:70)
<org.grails.plugins.databinding.DataBindingConfigurationSpec$Person@7ce8e48b name=inaccessible dob=inaccessible prop=inaccessible>

	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test customize data binding for the types which have standard ValueConverters using @Order(DataBindingConfigurationSpec.groovy:70)
Caused by: java.lang.NullPointerException: Cannot get property 'value' on null object
	... 1 more
DataBindingConfigurationSpec > test custom ValueConverter are ordered if defined with @Order (:grails-databinding:test in CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21))
Condition failed with Exception:

person.prop.value == "test2"
|      |    |
|      null java.lang.NullPointerException: Cannot get property 'value' on null object
|           	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test custom ValueConverter are ordered if defined with @Order(DataBindingConfigurationSpec.groovy:59)
<org.grails.plugins.databinding.DataBindingConfigurationSpec$Person@1b4c8306 name=inaccessible dob=inaccessible prop=inaccessible>

	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test custom ValueConverter are ordered if defined with @Order(DataBindingConfigurationSpec.groovy:59)
Caused by: java.lang.NullPointerException: Cannot get property 'value' on null object
	... 1 more
DataBindingConfigurationSpec > test customize data binding for the types which have standard ValueConverters using @Order (:grails-databinding:test in CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21))
Condition failed with Exception:

person.prop.value == "test2"
|      |    |
|      null java.lang.NullPointerException: Cannot get property 'value' on null object
|           	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test customize data binding for the types which have standard ValueConverters using @Order(DataBindingConfigurationSpec.groovy:70)
<org.grails.plugins.databinding.DataBindingConfigurationSpec$Person@4080bcd9 name=inaccessible dob=inaccessible prop=inaccessible>

	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test customize data binding for the types which have standard ValueConverters using @Order(DataBindingConfigurationSpec.groovy:70)
Caused by: java.lang.NullPointerException: Cannot get property 'value' on null object
	... 1 more
DataBindingConfigurationSpec > test custom ValueConverter are ordered if defined with @Order (:grails-databinding:test in CI - Groovy Joint Validation Build / build_grails)
Condition failed with Exception:

person.prop.value == "test2"
|      |    |
|      null java.lang.NullPointerException: Cannot get property 'value' on null object
|           	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test custom ValueConverter are ordered if defined with @Order(DataBindingConfigurationSpec.groovy:59)
<org.grails.plugins.databinding.DataBindingConfigurationSpec$Person@5398adc9 name=inaccessible dob=inaccessible prop=inaccessible>

	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test custom ValueConverter are ordered if defined with @Order(DataBindingConfigurationSpec.groovy:59)
Caused by: java.lang.NullPointerException: Cannot get property 'value' on null object
	... 1 more
DataBindingConfigurationSpec > test customize data binding for the types which have standard ValueConverters using @Order (:grails-databinding:test in CI - Groovy Joint Validation Build / build_grails)
Condition failed with Exception:

person.prop.value == "test2"
|      |    |
|      null java.lang.NullPointerException: Cannot get property 'value' on null object
|           	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test customize data binding for the types which have standard ValueConverters using @Order(DataBindingConfigurationSpec.groovy:70)
<org.grails.plugins.databinding.DataBindingConfigurationSpec$Person@4a39d06d name=inaccessible dob=inaccessible prop=inaccessible>

	at org.grails.plugins.databinding.DataBindingConfigurationSpec.test customize data binding for the types which have standard ValueConverters using @Order(DataBindingConfigurationSpec.groovy:70)
Caused by: java.lang.NullPointerException: Cannot get property 'value' on null object
	... 1 more
SecuredControllerSpec > test login as sherlock, sherlock belongs to detective groups. All detectives have the role ADMIN (:grails-test-examples-spring-security-core-misc-functional-test-app-group:integrationTest in CI - Groovy Joint Validation Build / build_grails)
Condition not satisfied:

pageSource.contains('you have ROLE_ADMIN')
|          |
|          false
<html lang="en" class="no-js"><head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>
        Login
    </title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
	<meta name="layout" content="main">
	
	<style type="text/css" media="screen">
 
	#login {
		margin: 15px 0px;
		padding: 0px;
		text-align: center;
		font-family: Arial, Helvetica, sans-serif;
	}
 
	#login .inner {
		width: 340px;
		padding-bottom: 6px;
		margin: 60px auto;
		text-align: left;
		border: 1px solid #aab;
		background-color: #f0f0fa;
		-moz-box-shadow: 2px 2px 2px #eee;
		-webkit-box-shadow: 2px 2px 2px #eee;
		-khtml-box-shadow: 2px 2px 2px #eee;
		box-shadow: 2px 2px 2px #eee;
	}
 
	#login .inner .fheader {
		padding: 18px 26px 14px 26px;
		background-color: #f7f7ff;
		margin: 0px 0 14px 0;
		color: #2e3741;
		font-size: 18px;
		font-weight: bold;
	}
 
	#login .inner .cssform p {
		clear: left;
		margin: 0;
		padding: 4px 0 3px 0;
		padding-left: 105px;
		margin-bottom: 20px;
		height: 1%;
	}
 
	#login .inner .cssform input[type="text"] {
		width: 75%;
	}
 
	#login .inner .cssform label {
		font-weight: bold;
		float: left;
		text-align: right;
		margin-left: -105px;
		width: 110px;
		padding-top: 3px;
		padding-right: 10px;
	}
 
	#remember_me_holder {
		padding-left: 120px;
	}
 
	#submit {
		margin-left: 15px;
	}
 
	#remember_me_holder label {
		float: none;
		margin-left: 0;
		text-align: left;
		width: 200px
	}
 
	#login .inner .login_message {
		padding: 6px 25px 20px 25px;
		color: #c33;
	}
 
	#login .inner .text_ {
		width: 75%;
	}
 
	#login .inner .chk {
		height: 12px;
	}
 
	#passwordToggler {
		padding: 3px 4px;
		cursor: pointer;
	}
	</style>
 
</head>
<body>
 
<div id="login">
	<div class="inner">
		<div class="fheader">Please Login</div>
 
		
 
		<form action="/login/authenticate" method="POST" id="loginForm" class="cssform" autocomplete="off">
			<p>
				<label for="username">Username:</label>
				<input type="text" class="text_" name="username" id="username" autocapitalize="none">
			</p>
 
			<p>
				<label for="password">Password:</label>
				<input type="password" class="text_" name="password" id="password">
				<i id="passwordToggler" title="toggle password display" onclick="passwordDisplayToggle()">👁</i>
			</p>
 
			<p id="remember_me_holder">
				<input type="checkbox" class="chk" name="remember-me" id="remember_me">
				<label for="remember_me">Remember me</label>
			</p>
 
			<p>
				<input type="submit" id="submit" value="Login">
			</p>
		</form>
	</div>
</div>
<script type="text/javascript">
	document.addEventListener("DOMContentLoaded", function(event) {
		document.forms['loginForm'].elements['username'].focus();
	});
 
	function passwordDisplayToggle() {
		var toggleEl = document.getElementById("passwordToggler");
		var eyeIcon = '\u{1F441}';
		var xIcon = '\u{2715}';
		var passEl = document.getElementById("password");
 
		if (passEl.type === "password") {
			toggleEl.innerHTML = xIcon;
			passEl.type = "text";
		} else {
			toggleEl.innerHTML = eyeIcon;
			passEl.type = "password";
		}
	}
</script>
 
 
 
</body></html>

	at demo.SecuredControllerSpec.$tt__$spock_feature_1_0(SecuredControllerSpec.groovy:52)
	at demo.SecuredControllerSpec.test login as sherlock, sherlock belongs to detective groups. All detectives have the role ADMIN_closure2(SecuredControllerSpec.groovy)
	at grails.gorm.transactions.GrailsTransactionTemplate$1.doInTransaction(GrailsTransactionTemplate.groovy:72)
	at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:137)
	at grails.gorm.transactions.GrailsTransactionTemplate.executeAndRollback(GrailsTransactionTemplate.groovy:69)
	at demo.SecuredControllerSpec.test login as sherlock, sherlock belongs to detective groups. All detectives have the role ADMIN(SecuredControllerSpec.groovy)
SpringSecurityServiceIntegrationSpec > update role (:grails-test-examples-spring-security-core-integration-test-app:integrationTest in CI - Groovy Joint Validation Build / build_grails)
Condition not satisfied:

newDescription == role.description
|              |  |    |
new description|  |    description
               |  TestRole(auth:ROLE_ADMIN)
               false
               4 differences (73% similarity)
               (new )description
               (----)description

	at grails.plugin.springsecurity.SpringSecurityServiceIntegrationSpec.update role(SpringSecurityServiceIntegrationSpec.groovy:97)
expected actual
description new description
SpringSecurityServiceIntegrationSpec > update role when invalid (:grails-test-examples-spring-security-core-integration-test-app:integrationTest in CI - Groovy Joint Validation Build / build_grails)
Condition not satisfied:

!success
||
|true
false

	at grails.plugin.springsecurity.SpringSecurityServiceIntegrationSpec.update role when invalid(SpringSecurityServiceIntegrationSpec.groovy:68)

Muted Tests

Select tests to mute in this pull request:

  • DataBindingConfigurationSpec > test custom ValueConverter are ordered if defined with @Order
  • DataBindingConfigurationSpec > test customize data binding for the types which have standard ValueConverters using @Order
  • SecuredControllerSpec > test login as sherlock, sherlock belongs to detective groups. All detectives have the role ADMIN
  • SpringSecurityServiceIntegrationSpec > update role
  • SpringSecurityServiceIntegrationSpec > update role when invalid

Reuse successful test results:

  • ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

  • Rerun jobs

Learn more about TestLens at testlens.app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants