Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6309f18
Add opt-in nullMissing support to bindData for stale-data clearing
jamesfredley Jul 9, 2026
018220f
Merge deny-by-default binding into nullMissing branch
jamesfredley Jul 17, 2026
7383989
Merge origin/8.0.x into fix/binddata-null-missing-stale-data
jamesfredley Jul 17, 2026
990a79d
Merge fix/binddata-mass-assignment into fix/binddata-null-missing-sta…
jamesfredley Jul 17, 2026
62d4aad
Merge branch 'fix/binddata-mass-assignment' into fix/binddata-null-mi…
jamesfredley Jul 29, 2026
59ec123
Merge branch 'fix/binddata-mass-assignment' into fix/binddata-null-mi…
jamesfredley Aug 2, 2026
1f56694
Merge branch 'fix/binddata-mass-assignment' into fix/binddata-null-mi…
jamesfredley Aug 2, 2026
cbfd385
Consolidate framework-managed data binding properties
jamesfredley Aug 2, 2026
dc719ce
Extract nullMissing property clearing
jamesfredley Aug 2, 2026
2b53efd
Fix domain binding spec cleanup lifecycle
jamesfredley Aug 2, 2026
50714be
Keep Grails-managed domain properties bindable when opted in
jamesfredley Aug 2, 2026
9cd1bde
Merge branch 'fix/binddata-mass-assignment' into fix/binddata-null-mi…
jamesfredley Aug 5, 2026
69ed46e
Merge binding security updates into nullMissing work
jamesfredley Aug 6, 2026
bcb05af
Merge branch 'fix/binddata-mass-assignment' into fix/binddata-null-mi…
jamesfredley Aug 6, 2026
8cc9cee
Rename `nullMissing` to `clearMissing`
matrei Aug 11, 2026
c4e58c2
docs: clarify behavior of clearMissing in FrameworkPropertyNames
matrei Aug 11, 2026
3352c00
test: improve clarity of test names for clearMissing behavior
matrei Aug 11, 2026
e133a02
docs: enhance bindData documentation with wildcard usage and clearMis…
matrei Aug 11, 2026
0c87633
test: improve clarity of bindData test names and enhance clearMissing…
matrei Aug 11, 2026
1309165
feat: implement wildcard expansion for included properties in clearMi…
matrei Aug 11, 2026
4633c67
docs: clarify behavior of clearMissing for Grails-managed properties
matrei Aug 11, 2026
ecff934
chore: cleanup `DefaultASTDatabindingHelperDomainClassSpecialProperti…
matrei Aug 14, 2026
a6f4846
Merge branch '8.0.x' into fix/binddata-null-missing-stale-data
jamesfredley Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package grails.databinding;

import java.util.Set;

/**
* Property names managed by the language runtime or Grails rather than ordinary request data.
* <p>
* Intrinsic runtime properties are never request-bindable. Grails-managed domain properties
* are excluded from generated allowlists by default, but may still bind and be cleared when an
* application explicitly opts them in (for example {@code bindable: true}); intrinsic runtime
* properties remain protected.
*/
public final class FrameworkPropertyNames {

/**
* Language / MetaClass properties that must never be bound from request data.
*/
public static final Set<String> INTRINSIC_RUNTIME_PROPERTIES = Set.of(
"class", "classLoader", "protectionDomain", "metaClass", "metaPropertyValues", "properties");

/**
* Grails domain lifecycle properties excluded from default binding allowlists but eligible
* for {@code clearMissing} when explicitly included.
*/
public static final Set<String> GRAILS_MANAGED_PROPERTIES = Set.of(
"errors", "id", "version", "dateCreated", "lastUpdated");

/**
* Union of {@link #INTRINSIC_RUNTIME_PROPERTIES} and {@link #GRAILS_MANAGED_PROPERTIES}.
*/
public static final Set<String> FRAMEWORK_MANAGED_PROPERTIES = Set.of(
"class", "classLoader", "protectionDomain", "metaClass", "metaPropertyValues", "properties",
"errors", "id", "version", "dateCreated", "lastUpdated");

private FrameworkPropertyNames() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -271,19 +271,25 @@ class SimpleDataBinder implements DataBinder {
}

protected boolean isOkToBind(String propName, List whiteList, List blackList) {
'class' != propName && 'classLoader' != propName && 'protectionDomain' != propName && 'metaClass' != propName && 'metaPropertyValues' != propName && 'properties' != propName && !blackList?.contains(propName) && (whiteList == null || isBindAllBindingIncludeList(whiteList) || whiteList.contains(propName) || whiteList.find { it -> it?.toString()?.startsWith(propName + '.') })
// Only intrinsic runtime properties are hard-denied here. Grails-managed domain
// properties (id, version, dateCreated, lastUpdated, errors) may still bind when
// explicitly allowlisted (e.g. bindable: true); intrinsic runtime properties remain
// hard-denied while Grails-managed properties follow the explicit binding allowlist.
!FrameworkPropertyNames.INTRINSIC_RUNTIME_PROPERTIES.contains(propName) && !blackList?.contains(propName) &&
(whiteList == null || isBindAllBindingIncludeList(whiteList) || whiteList.contains(propName) ||
whiteList.any { item -> item?.toString()?.startsWith(propName + '.') })
}

/**
* Marker include list meaning "bind every eligible property". Used when an
* explicit exclude-only bind must not intersect the class allowlist.
*/
static List getBindAllBindingIncludeList() {
protected static List getBindAllBindingIncludeList() {
BIND_ALL_BINDING_INCLUDE_LIST
}

static boolean isBindAllBindingIncludeList(List includeList) {
includeList instanceof BindAllBindingIncludeList
protected static boolean isBindAllBindingIncludeList(List includeList) {
includeList.is(BIND_ALL_BINDING_INCLUDE_LIST)
}

private static final class BindAllBindingIncludeList extends ArrayList {
Expand Down
3 changes: 3 additions & 0 deletions grails-doc/src/en/guide/upgrading/upgrading80x.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,9 @@ The Spring annotations still work, so this is non-blocking, but new code should
* **Namespaced link generation is namespace-aware.**
When a link, form action, pagination link, sortable column link, redirect, chain, or include targets a controller without an explicit `namespace`, Grails now resolves the namespace automatically. In the normal case, where only one controller has the target name, `controller` and `action` generate the correct namespaced or non-namespaced URL. Ambiguity only occurs when multiple controllers share the same name. In that case, specify `namespace` to choose the target explicitly. Pass `namespace: null` from Groovy code or `namespace=""` in a GSP tag to target the non-namespaced controller explicitly.

* **`bindData` can clear omitted included fields.**
`bindData(target, source, [include: [...], clearMissing: true])` now clears included properties that are absent from the binding source. The behavior is opt-in, requires an `include` list, and does not apply globally. Existing `bindData` calls without `clearMissing: true` keep omitted fields unchanged.

==== 21. Tag Library Test Cleanup Changes

Grails 8 removes the `purgeTagLibMetaClass` test hook used by some web and TagLib unit tests.
Expand Down
20 changes: 19 additions & 1 deletion grails-doc/src/en/ref/Controllers/bindData.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ bindData(target, params, [exclude: ['firstName', 'lastName']], "author")

// using inclusive map
bindData(target, params, [include: ['firstName', 'lastName']], "author")

// clear included properties omitted from the source
bindData(target, params, [include: ['firstName', 'lastName'], clearMissing: true])
----


Expand All @@ -57,7 +60,7 @@ Arguments:

* `target` - The target object to bind to
* `params` - A `Map` of source parameters, often the link:params.html[params] object when used in a controller
* `includesExcludes` - (Optional) A map with 'include' and/or 'exclude' lists containing the names of properties to either include or exclude.
* `includesExcludes` - (Optional) A map with 'include' and/or 'exclude' lists containing the names of properties to either include or exclude. Set `clearMissing: true` with an `include` list to clear included properties that are omitted from the binding source.
* `prefix` - (Optional) A string representing a prefix to use to filter parameters. The method will automatically append a '.' when matching the prefix to parameters, so you can use 'author' to filter for parameters such as 'author.name'.

If no `include` list is supplied, `bindData` uses the target class default binding behavior. By default, statically typed instance properties bind for compatibility unless they are marked `bindable: false`. Existing `bindable: true` declarations and explicit `include` lists continue to bind exactly the properties they name without configuration changes. An empty `include` list binds no properties.
Expand All @@ -73,6 +76,19 @@ Use `include` to allow only the properties needed for a request:
bindData(target, params, [include: ['firstName', 'lastName']])
----

Include entries may use wildcard suffixes for nested properties. `address.*` includes all
properties nested under `address`, while `address_*` is the corresponding underscore-form
used for nested binding paths and generated binding allowlists:

[source,groovy]
----
bindData(target, params, [include: ['address.*']])
bindData(target, params, [include: ['address_*']])
----

When `clearMissing: true` is used, omitted properties matched by either wildcard form are
cleared, subject to the normal `exclude` and bindability rules.

For controller action command object parameters, use `grails.web.databinding.BindAllowed` to allow request binding for only the listed properties:

[source,groovy]
Expand All @@ -88,6 +104,8 @@ class PersonController {

See the link:{constraintsRefFromRef}bindable.html[bindable] constraint documentation for more information on controlling default bindability. Applications may set `grails.databinding.denyByDefault=true` to opt into deny-by-default binding allowlists. In secure mode, permit a property with `bindable: true`, an explicit `include` list, or `@BindAllowed` on a controller action command object parameter.

`clearMissing` is opt-in and only applies when an `include` list is provided. This is useful for update forms where an omitted allowed field should clear an existing value instead of leaving stale persisted data. Excluded properties are not cleared.

Only boolean values and the strings `'true'` and `'false'` are recognised for `grails.databinding.denyByDefault`. String matching ignores case and surrounding whitespace. An unrecognised value logs a warning and enables secure binding.

The underlying implementation uses Spring's Data Binding framework. If the target is a domain class, type conversion errors are stored in the `errors` property of the domain class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,44 @@
*/
package org.grails.web.binding

import groovy.transform.CompileStatic

import spock.lang.Issue
import spock.lang.Specification

import grails.config.Settings
import grails.gorm.dirty.checking.DirtyCheck
import grails.persistence.Entity
import grails.util.Holders
import groovy.transform.CompileStatic
import grails.web.databinding.DataBindingUtils
import grails.web.databinding.GrailsWebDataBinder
import org.grails.config.PropertySourcesConfig
import org.grails.validation.ConstraintEvalUtils
import spock.lang.Issue
import spock.lang.Specification

class DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec extends
Specification {
class DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec extends Specification {

private def originalConfig

def setup() {
ConstraintEvalUtils.clearDefaultConstraints()
originalConfig = Holders.config
Holders.setConfig(new PropertySourcesConfig([(Settings.DATABINDING_DENY_BY_DEFAULT): true]))
grails.web.databinding.DataBindingUtils.clearBindingCaches()
grails.web.databinding.GrailsWebDataBinder.resetWarnedBindingShapes()
Holders.config = new PropertySourcesConfig([(Settings.DATABINDING_DENY_BY_DEFAULT): true])
DataBindingUtils.clearBindingCaches()
GrailsWebDataBinder.resetWarnedBindingShapes()
}

def cleanup() {
ConstraintEvalUtils.clearDefaultConstraints()
Holders.setConfig(originalConfig)
grails.web.databinding.DataBindingUtils.clearBindingCaches()
grails.web.databinding.GrailsWebDataBinder.resetWarnedBindingShapes()
Holders.config = originalConfig
DataBindingUtils.clearBindingCaches()
GrailsWebDataBinder.resetWarnedBindingShapes()
}

@Issue('GRAILS-11173')
void 'Test binding to special properties in a domain class'() {
when:
Date now = new Date()
SomeDomainClass obj = new SomeDomainClass(dateCreated: now, lastUpdated: now)
def now = new Date()
def obj = new SomeDomainClass(dateCreated: now, lastUpdated: now)

then:
obj.dateCreated == null
Expand Down Expand Up @@ -140,8 +143,7 @@ class DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec extends

void 'Test unconfigured binding remains permissive and preserves bindable false'() {
given:
def configuredConfig = Holders.config
Holders.setConfig(null)
Holders.config = null

when:
def obj = new DomainWithSecureBindableDefault(name: 'Grace', title: 'Admiral', role: 'Admin')
Expand All @@ -150,9 +152,6 @@ class DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec extends
obj.name == 'Grace'
obj.title == 'Admiral'
obj.role == null

cleanup:
Holders.setConfig(configuredConfig)
}

@Issue('https://github.com/apache/grails-core/issues/15795')
Expand Down
Loading
Loading