Skip to content

Reject invalid databinding indexes - #15804

Closed
jamesfredley wants to merge 3 commits into
8.0.xfrom
fix/databinding-negative-index
Closed

Reject invalid databinding indexes#15804
jamesfredley wants to merge 3 commits into
8.0.xfrom
fix/databinding-negative-index

Conversation

@jamesfredley

@jamesfredley jamesfredley commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Description

What was found

Indexed data-binding paths accepted index text before consistently validating it, which produced two unsafe outcomes:

  • Negative indexes like books[-1].title used Groovy negative-index semantics and mutated the last existing element.
  • Malformed indexes like books[bad].title could throw during binding instead of being reported as binding errors.

Because binding paths commonly come from request parameters, the binder should not let externally supplied negative indexes select an element different from the literal index.

What changed

  • Add a shared indexed-property parser that accepts only non-negative integer indexes.
  • Report invalid indexed paths as binding errors and skip the mutation.
  • Apply the guard before array, collection, and web-domain-association access.
  • Leave map-key binding unchanged because map keys are not numeric collection indexes.
  • Document the non-negative integer index requirement and the map-key exception.

Compatibility note

Negative collection indexes likely worked in prior Grails versions through Groovy list semantics. This branch intentionally treats them as invalid binding input for Grails 8, and the behavior question remains open for maintainer discussion.

Verification

  • ./gradlew --no-daemon :grails-databinding-core:test --tests "grails.databinding.CollectionBindingSpec" :grails-test-suite-persistence:test --tests "grails.web.databinding.GrailsWebDataBinderSpec" :grails-web-databinding:check
  • The persistence-suite GrailsWebDataBinderSpec covers the web-domain-association path that Codecov reports outside local grails-web-databinding module coverage.

Contributor Checklist

Issue and Scope

  • This PR has no acknowledged issue yet; the background above explains why the change is necessary.
  • This PR addresses invalid indexed collection/array/association binding paths.
  • This PR contains a single focused databinding hardening change.
  • This PR targets 8.0.x, where behavior-hardening changes are permitted for discussion.

Code Quality

  • I have added or updated tests that cover the changes introduced in this PR.
  • I ran the focused verification listed above.
  • The change follows the project's code style and avoids mass reformatting.
  • Generative AI tooling was used as an ai-generated starting point and reviewed before submission.

Licensing and Attribution

  • All contributed code is provided under the Apache License 2.0.
  • I have the necessary rights to submit this contribution and confirm it is my own original work.
  • Generative AI tooling use is labeled on the PR.

Documentation

  • User-facing data-binding behavior is documented in the data binding guide.
  • The PR description explains what changed and why.

Rebased on 8.0.x

Merged the latest 8.0.x (clean, no conflicts) and re-ran CollectionBindingSpec + GrailsWebDataBinderSpec successfully. No Copilot comments; the negative-index compatibility change remains open for weekly-meeting discussion.

Report binding errors for malformed or negative indexed binding paths before array, collection, or domain association access.
Apply the same guard to the web data binder association path and cover the core and web binding behavior with regressions.

Assisted-by: Hephaestus:openai/gpt-5.5
Copilot AI review requested due to automatic review settings July 1, 2026 12:18

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 data binding by rejecting malformed or negative indexed binding paths (e.g., list[-1], list[bad]) before attempting array/collection/domain-association access, converting what previously could throw or apply Groovy negative-index semantics into binding errors captured by the binding error listener flow.

Changes:

  • Add parseIndexedPropertyIndex(...) to centralize index parsing/validation and emit binding errors on malformed/negative indexes.
  • Use the new index parsing in both core (SimpleDataBinder) and web (GrailsWebDataBinder) indexed-binding paths to prevent invalid index access.
  • Add regression tests covering negative and malformed indexes for Lists, arrays, and domain association collection binding.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
grails-web-databinding/src/main/groovy/grails/web/databinding/GrailsWebDataBinder.groovy Validates indexed binding paths for domain association collections before attempting indexed updates.
grails-databinding-core/src/main/groovy/grails/databinding/SimpleDataBinder.groovy Introduces shared index parsing/validation and uses it for array/collection indexed binding.
grails-databinding-core/src/test/groovy/grails/databinding/CollectionBindingSpec.groovy Adds regression coverage ensuring negative/malformed indexes are rejected for List and array binding.
grails-test-suite-persistence/src/test/groovy/grails/web/databinding/GrailsWebDataBinderSpec.groovy Adds web-layer regression coverage for rejecting invalid indexed domain association binding to Lists.

💡 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

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.0000%. Comparing base (b980413) to head (e0d3624).
⚠️ Report is 114 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@      Coverage Diff       @@
##   8.0.x   #15804   +/-   ##
==============================
==============================
🚀 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.

@jdaugherty

Copy link
Copy Markdown
Contributor

Didn't this work in prior Grails versions? The negative index could be used to bind to the end of an array. I agree this is probably a good change, but it needs discussed. Also, this PR is not adhering to our PR policy - we need explanations for why this is necessary, etc. There are several checklists when creating a PR that are not completed by this PR.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Thanks. I updated the PR body to follow the summary policy more explicitly: it now separates "What was found", "What was fixed", and "Verification".

Yes, negative collection indexes likely worked before via Groovy list semantics. The concern is that indexed binding paths come from request parameters, so items[-1] can mutate the last existing element instead of being treated as an invalid user-supplied index. I agree this behavior change should be discussed by maintainers, so I am leaving that discussion open rather than marking it resolved.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Maintenance pass complete. Copilot generated no inline review comments and there are no review threads to resolve. I reviewed the Codecov patch-coverage note for GrailsWebDataBinder: the behavior is covered by the persistence-suite GrailsWebDataBinderSpec tests rather than local grails-web-databinding module coverage, and I reran the focused CollectionBindingSpec plus GrailsWebDataBinderSpec successfully. The maintainer question about negative-index compatibility remains open as a behavior discussion, not resolved.

Document that indexed binding to arrays, collections, and many-ended
associations requires non-negative integer indexes, that invalid indexes
are reported as binding errors without mutating the target, and that map
keys are unaffected. Align the Set binding example comment with the
integer index requirement.

Assisted-by: opencode:gpt-5.5
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Pre-release review pass (2026-07-02, dual-reviewer: GPT-5.5 oracle + Codex CLI): NEEDS-IMPROVEMENT, now addressed.

Findings: the user-facing behavior change (negative/malformed indexes rejected as binding errors instead of Groovy negative-index semantics or exceptions) was undocumented in the data binding guide (high, docs-only) - fixed in e2e56748f3, including aligning the stale Set example comment ("values can be anything") with the actual integer-index requirement. Reviewers confirmed the rejection design (binding error + skip, no mutation) matches existing binding contracts, the guard covers core and web-binder association paths, and positive oversized indexes remain governed by the pre-existing autoGrowCollectionLimit cap (not new in this PR).

@jdaugherty

Copy link
Copy Markdown
Contributor

@jamesfredley please fix the formatting on this summary

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Updated the summary formatting to follow the repository PR template and added the missing checklist context.

I also reran the focused coverage for the invalid-index behavior. The tests cover the core list/array paths and the web-domain-association path in GrailsWebDataBinderSpec; the latter lives in the persistence suite, which is why Codecov can show the local grails-web-databinding module lines as uncovered while the behavior is still tested.

Verification: ./gradlew --no-daemon :grails-databinding-core:test --tests "grails.databinding.CollectionBindingSpec" :grails-test-suite-persistence:test --tests "grails.web.databinding.GrailsWebDataBinderSpec" :grails-web-databinding:check passed.

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

I'm ok with this change, but it is removing a feature that existed before. We should make sure this is raised in the weekly meeting.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Maintenance pass

Merged the latest 8.0.x (a large base gap - clean merge, no conflicts) and re-verified. Copilot generated no inline comments and there are no open review threads.

  • :grails-databinding-core:test --tests CollectionBindingSpec and :grails-test-suite-persistence:test --tests GrailsWebDataBinderSpec both pass after the merge, including the negative/malformed index rejection coverage for List, array, and domain-association binding.

The maintainer question about negative-index compatibility (previously items[-1] could bind to the end of a collection) remains an intentional, user-visible behavior change and is left open for discussion in the weekly meeting rather than resolved here.

@testlens-app

testlens-app Bot commented Jul 16, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: e0d3624
▶️ Tests: 9360 executed
⚪️ Checks: 59/59 completed


Learn more about TestLens at testlens.app.

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

Withdrawing my earlier request to raise this at the weekly meeting. The JavaBeans specification (v1.01, section 7.2) defines indexed properties as array-typed properties with paired int-indexed accessors, where an invalid index may throw ArrayIndexOutOfBoundsException - negative indexes were never part of that model. Grails extends indexed binding beyond arrays to collections, and this change aligns that extension with the spec's array semantics; the old books[-1] behavior was Groovy's putAt(-1) list extension leaking through the binder, not a supported feature. That makes the negative-index rejection an alignment with the beans model rather than a feature removal, and no further discussion is needed. Please update the PR description's compatibility note to reference the spec section instead of leaving the behavior question open. One genuine compatibility issue remains on the Set association path, which the spec does not cover in either direction - see the inline comment.

}
}

protected Integer parseIndexedPropertyIndex(obj, IndexedPropertyReferenceDescriptor indexedPropertyReferenceDescriptor,

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.

Rejecting negative indexes is the correct behavior: the JavaBeans specification (v1.01, section 7.2) defines indexed properties as array-typed properties with paired int-indexed accessors, where an invalid index may throw ArrayIndexOutOfBoundsException. Grails' indexed binding to collections is an extension of that model, and this change aligns the extension with the spec's array semantics - the prior [-1] behavior was Groovy list semantics leaking through, never valid under the beans model. Please reference the spec section in this method's groovydoc and in the PR description so the rationale is on record.

try {
Integer index = Integer.parseInt(indexedPropertyReferenceDescriptor.index)
if (index < 0) {
throw new NumberFormatException(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.

Since this path binds untrusted request data, the reported binding error must stay generic - indistinguishable from the malformed-index case - so the source cannot tell we handle negative indexes explicitly. The current shape achieves that, but only by accident of throwing NumberFormatException as control flow. Please add a code comment documenting that the uniform error is intentional, so a future cleanup does not "improve" it into a distinct, more descriptive message.

if (referencedType != null && isDomainClass(referencedType)) {
needsBinding = false
if (Set.isAssignableFrom(metaProperty.type)) {
Integer index = parseIndexedPropertyIndex(obj, indexedPropertyReferenceDescriptor, val, listener, errors)

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.

This changes documented behavior that the JavaBeans rationale does not cover in either direction: spec indexed properties are array-typed with int accessors, so Set binding keys have no spec standing at all - the arbitrary-unique-key convention here is purely Grails' own documented contract (the guide says the values "can be anything as long as they are unique within the Map"). The old code only reached Integer.parseInt in the add-queried-instance path, so updating an existing Set element by id (e.g. albums[foo]: [id: 1, ...]) never parsed the index and worked with non-numeric keys; parsing at the top of the branch now rejects those previously valid paths. Requiring a non-negative integer here also falsely implies the key is positional when a Set has no positions. Either defer the parse to the addElementToCollectionAt call as before, or treat Set keys like map keys.


That code would work in the same way if `albums` were an array instead of a `List`.

NOTE: When binding to an array, a `Collection`, or a many-ended domain association by index, the value inside square brackets must be a non-negative integer. Entries such as `albums[-1]` or `albums[bogus]` are rejected as binding errors. The error field name includes the offending indexed segment, that binding path is skipped, and the target array, collection, or association is not changed by that entry. Map keys are not interpreted as numeric indexes, so keys such as `players[guitar]` remain valid map keys.

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.

State the rationale here rather than presenting this as an arbitrary rule: array and positional collection binding follows the JavaBeans indexed-property model (spec v1.01, section 7.2), which only defines non-negative int indexes with array semantics. Note the spec does not cover Set binding at all - see my comment on the Set branch in GrailsWebDataBinder. If Set keys remain arbitrary grouping keys per the existing documented contract, the change below to "non-negative integers that only need to be unique" overstates the restriction and should be reverted for the Set case.

@sanjana2505006

Copy link
Copy Markdown
Contributor

Opened PR #16058 with the remaining review items from this thread.

@borinquenkid

Copy link
Copy Markdown
Member

Closing in favor of #16058, which continues this work and addresses @jdaugherty's 2026-07-16 review feedback (JavaBeans §7.2 citation, the intentional-generic-error comment, and reverting the non-negative-integer requirement for Set association keys).

@github-project-automation github-project-automation Bot moved this from Todo to Done in Apache Grails Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants