Skip to content

Add explicit data binding API#15808

Open
jamesfredley wants to merge 11 commits into
8.0.xfrom
feat/secure-bind-data
Open

Add explicit data binding API#15808
jamesfredley wants to merge 11 commits into
8.0.xfrom
feat/secure-bind-data

Conversation

@jamesfredley

@jamesfredley jamesfredley commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

The Problem

Grails bindData binds 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 secureBindData API that makes the allow-list mandatory and verifies it at compile time.

  • Explicit allow-list required - secureBindData will not bind without an explicit list of permitted parameter names, so domains and command objects are never exposed to mass assignment by accident.
  • Compile-time enforcement - a controller secureBindData call 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 a static final constant 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.
  • Full binding coverage - supports indexed, nested, typed-map, collection, request-body, and prefix-filter binding scenarios, plus @BindUsing / @BindingFormat properties.
  • nullMissing - allowed fields absent from the request can be intentionally cleared.

Hardening found during review

Red regression tests were written first, then fixed:

  • Out-of-range indexed access - an indexed collection path beyond autoGrowCollectionLimit (silently skipped by the binder) previously threw IndexOutOfBoundsException during nullMissing processing. Now treated as absent.
  • Non-numeric bracket index - items[bad] against a List/array property threw an uncaught NumberFormatException in the pre-existing SimpleDataBinder, which caused the generic catch in DataBindingUtils to discard the whole BindingResult. Now guarded (fixes both bindData and secureBindData).
  • Null binding-error message - the generic exception fallback passed e.getMessage() (which can be null) straight into ObjectError, yielding empty/unhelpful binding errors. Added resolveBindingErrorMessage to fall back to the exception class name.

Documentation

New secureBindData controller reference page, plus data-binding guide and reference coverage. Docs warn that the allow-list must be developer-controlled (a literal or static final constant), never built from request/params data - 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 + hibernate7 AdvancedDataBindingSpec integration specs pass.

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
Copilot AI review requested due to automatic review settings July 1, 2026 23:00

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 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 secureBindData APIs (object + collection binding) and corresponding secure binding/filtering logic in DataBindingUtils.
  • Enforce controller compile-time validation: calling secureBindData without an explicit allowedParams list becomes a compilation error.
  • Add extensive unit/integration tests and documentation for secureBindData, including nullMissing, 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

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 17.76316% with 625 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.2207%. Comparing base (66cd24c) to head (3f61e1c).

Files with missing lines Patch % Lines
...roovy/grails/web/databinding/DataBindingUtils.java 0.8681% 571 Missing ⚠️
...ails/compiler/web/ControllerActionTransformer.java 69.3182% 21 Missing and 33 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                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     
Files with missing lines Coverage Δ
.../groovy/grails/databinding/SimpleDataBinder.groovy 75.6345% <100.0000%> (+0.3768%) ⬆️
...ails/compiler/web/ControllerActionTransformer.java 68.4295% <69.3182%> (+0.3491%) ⬆️
...roovy/grails/web/databinding/DataBindingUtils.java 10.1530% <0.8681%> (-37.8470%) ⬇️

... and 2 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.

borinquenkid and others added 2 commits July 1, 2026 19:26
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>
@jamesfredley

Copy link
Copy Markdown
Contributor Author

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.)

Area Finding Severity
ControllerActionTransformer compile-time guard Accepts arbitrary expressions / mutable List locals as allowedParams, including values derived from params - defeating the explicit-allowlist goal (matches the added failing GAP test). Require literal string-constant lists or recognized static final constants; reject request-derived expressions. high
DataBindingUtils nullMissing indexed paths Malformed / non-numeric / out-of-range list indexes can throw or produce binding errors via Integer.parseInt / List.set; the added GAP tests expect these to be ignored, so CI was red at review time. high
CI Multiple failing checks at the reviewed head (incl. the new GAP tests). high
Test coverage No secure-path tests proving allowed properties still flow through @BindUsing / @BindingFormat and disallowed formatted/custom-bound properties stay blocked. med
secureBindData docs Present (good), but should warn that allowedParams must be developer-controlled - never built from params/request input. med

Positive: the distinct secureBindData name is discoverable next to bindData, empty allowlists bind nothing, and nested/indexed allowlist semantics are documented. The API direction is good; the compile-time guard and red CI are the gate.

@borinquenkid

Copy link
Copy Markdown
Member

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.)

Area Finding Severity
ControllerActionTransformer compile-time guard Accepts arbitrary expressions / mutable List locals as allowedParams, including values derived from params - defeating the explicit-allowlist goal (matches the added failing GAP test). Require literal string-constant lists or recognized static final constants; reject request-derived expressions. high
DataBindingUtils nullMissing indexed paths Malformed / non-numeric / out-of-range list indexes can throw or produce binding errors via Integer.parseInt / List.set; the added GAP tests expect these to be ignored, so CI was red at review time. high
CI Multiple failing checks at the reviewed head (incl. the new GAP tests). high
Test coverage No secure-path tests proving allowed properties still flow through @BindUsing / @BindingFormat and disallowed formatted/custom-bound properties stay blocked. med
secureBindData docs Present (good), but should warn that allowedParams must be developer-controlled - never built from params/request input. med
Positive: the distinct secureBindData name is discoverable next to bindData, empty allowlists bind nothing, and nested/indexed allowlist semantics are documented. The API direction is good; the compile-time guard and red CI are the gate.

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 jdaugherty 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.

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)

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.

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.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

@jdaugherty bindData has always had two security landmines from my standpoint, based on how the user implements them and unfortunately by default, that I always work around. I am trying to introduce an improved version, we can call it something else, as a new surface to use that eventually could become the default in Grails 9 or 10.

Since bindData does not require an allow list, by default it is susceptible to mass-assignment of command and domain fields. I always use an allow list, but I believe we will need to move towards making that required in the long term or at a minimum the default.

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 bindData by using an allow list and wrapping some extra code to null values that are expected in the request. I'd like to expose this and then potentially move towards it being the default down the road. The goal of this pr is to wrap bindData as much as possible, for now.

@jdaugherty

Copy link
Copy Markdown
Contributor

@jdaugherty bindData has always had two security landmines from my standpoint, based on how the user implements them and unfortunately by default, that I always work around. I am trying to introduce an improved version, we can call it something else, as a new surface to use that eventually could become the default in Grails 9 or 10.

Since bindData does not require an allow list, by default it is susceptible to mass-assignment of command and domain fields. I always use an allow list, but I believe we will need to move towards making that required in the long term or at a minimum the default.

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 bindData by using an allow list and wrapping some extra code to null values that are expected in the request. I'd like to expose this and then potentially move towards it being the default down the road. The goal of this pr is to wrap bindData as much as possible, for now.

I am fine creating aliases that make this easier to call, but calling it secure implies the other is insecure, which is false.

@jamesfredley jamesfredley changed the title Add secure explicit data binding API Add explicit data binding API Jul 2, 2026
jamesfredley and others added 4 commits July 2, 2026 09:45
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.
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
@testlens-app

testlens-app Bot commented Jul 2, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 3f61e1c
▶️ Tests: 49669 executed
⚪️ Checks: 46/46 completed


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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants