-
-
Notifications
You must be signed in to change notification settings - Fork 973
Reject invalid databinding indexes #15804
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -333,13 +333,19 @@ class SimpleDataBinder implements DataBinder { | |
| } | ||
|
|
||
| if (propertyType.isArray()) { | ||
| def index = Integer.parseInt(indexedPropertyReferenceDescriptor.index) | ||
| Integer index = parseIndexedPropertyIndex(obj, indexedPropertyReferenceDescriptor, val, listener, errors) | ||
| if (index == null) { | ||
| return | ||
| } | ||
| def array = initializeArray(obj, propName, propertyType.componentType, index) | ||
| if (array != null) { | ||
| addElementToArrayAt(array, index, val) | ||
| } | ||
| } else if (Collection.isAssignableFrom(propertyType)) { | ||
| def index = Integer.parseInt(indexedPropertyReferenceDescriptor.index) | ||
| Integer index = parseIndexedPropertyIndex(obj, indexedPropertyReferenceDescriptor, val, listener, errors) | ||
| if (index == null) { | ||
| return | ||
| } | ||
| Collection collectionInstance = initializeCollection(obj, propName, propertyType) | ||
| def indexedInstance = null | ||
| if (!(Set.isAssignableFrom(propertyType))) { | ||
|
|
@@ -394,6 +400,22 @@ class SimpleDataBinder implements DataBinder { | |
| } | ||
| } | ||
|
|
||
| protected Integer parseIndexedPropertyIndex(obj, IndexedPropertyReferenceDescriptor indexedPropertyReferenceDescriptor, | ||
| val, DataBindingListener listener, errors) { | ||
|
|
||
| try { | ||
| Integer index = Integer.parseInt(indexedPropertyReferenceDescriptor.index) | ||
| if (index < 0) { | ||
| throw new NumberFormatException(indexedPropertyReferenceDescriptor.index) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| } | ||
| index | ||
| } | ||
| catch (NumberFormatException e) { | ||
| addBindingError(obj, indexedPropertyReferenceDescriptor.toString(), val, e, listener, errors) | ||
| null | ||
| } | ||
| } | ||
|
|
||
| @CompileStatic(TypeCheckingMode.SKIP) | ||
| protected initializeArray(obj, String propertyName, Class arrayType, int index) { | ||
| Object[] array = obj[propertyName] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -128,13 +128,15 @@ assert band.albums[1].numberOfTracks == 7 | |
|
|
||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| Note that when binding to a `Set` the structure of the `Map` being bound to the `Set` is the same as that of a `Map` being bound to a `List` but since a `Set` is unordered, the indexes don't necessarily correspond to the order of elements in the `Set`. In the code example above, if `albums` were a `Set` instead of a `List`, the `bindingMap` could look exactly the same but 'Foxtrot' might be the first album in the `Set` or it might be the second. When updating existing elements in a `Set` the `Map` being assigned to the `Set` must have `id` elements in it which represent the element in the `Set` being updated, as in the following example: | ||
|
|
||
| [source,groovy] | ||
| ---- | ||
| /* | ||
| * The value of the indexes 0 and 1 in albums[0] and albums[1] are arbitrary | ||
| * values that can be anything as long as they are unique within the Map. | ||
| * non-negative integers that only need to be unique within the Map. | ||
| * They do not correspond to the order of elements in albums because albums | ||
| * is a Set. | ||
| */ | ||
|
|
@@ -516,7 +518,7 @@ class AccountingController { | |
| ==== Data binding and type conversion errors | ||
|
|
||
|
|
||
| Sometimes when performing data binding it is not possible to convert a particular String into a particular target type. This results in a type conversion error. Grails will retain type conversion errors inside the link:{domainClassesRef}errors.html[errors] property of a Grails domain class. For example: | ||
| Sometimes when performing data binding it is not possible to convert a particular String into a particular target type. This results in a type conversion error. Grails will retain type conversion errors inside the link:{domainClassesRef}errors.html[errors] property of a Grails domain class. Invalid indexed array, collection, and many-ended association binding paths, such as negative or non-integer indexes, are also retained as binding errors, and the field name identifies the indexed path that was rejected. For example: | ||
|
|
||
| [source,groovy] | ||
| ---- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -434,6 +434,10 @@ class GrailsWebDataBinder extends SimpleDataBinder { | |
| if (referencedType != null && isDomainClass(referencedType)) { | ||
| needsBinding = false | ||
| if (Set.isAssignableFrom(metaProperty.type)) { | ||
| Integer index = parseIndexedPropertyIndex(obj, indexedPropertyReferenceDescriptor, val, listener, errors) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| if (index == null) { | ||
| return | ||
| } | ||
| def collection = initializeCollection(obj, propName, metaProperty.type) | ||
| def instance | ||
| if (collection != null) { | ||
|
|
@@ -448,7 +452,7 @@ class GrailsWebDataBinder extends SimpleDataBinder { | |
| Exception e = new IllegalArgumentException(message) | ||
| addBindingError(obj, propName, idValue, e, listener, errors) | ||
| } else { | ||
| addElementToCollectionAt(obj, propName, collection, Integer.parseInt(indexedPropertyReferenceDescriptor.index), instance) | ||
| addElementToCollectionAt(obj, propName, collection, index, instance) | ||
| } | ||
| } | ||
| if (instance != null) { | ||
|
|
@@ -459,8 +463,11 @@ class GrailsWebDataBinder extends SimpleDataBinder { | |
| } | ||
| } | ||
| } else if (Collection.isAssignableFrom(metaProperty.type)) { | ||
| Integer idx = parseIndexedPropertyIndex(obj, indexedPropertyReferenceDescriptor, val, listener, errors) | ||
| if (idx == null) { | ||
| return | ||
| } | ||
| def collection = initializeCollection(obj, propName, metaProperty.type) | ||
| def idx = Integer.parseInt(indexedPropertyReferenceDescriptor.index) | ||
| if ('null' == idValue) { | ||
| if (idx < collection.size()) { | ||
| def element = collection[idx] | ||
|
|
||
There was a problem hiding this comment.
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 throwArrayIndexOutOfBoundsException. 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.