Browse Source

Improve documentation and matching algorithm in data binders

pull/27953/head
Sam Brannen 3 years ago
parent
commit
a7cf19cec5
  1. 7
      spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java
  2. 117
      spring-context/src/main/java/org/springframework/validation/DataBinder.java
  3. 262
      spring-context/src/test/java/org/springframework/validation/DataBinderTests.java
  4. 11
      spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java
  5. 11
      spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java
  6. 1
      spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java
  7. 22
      spring-web/src/main/java/org/springframework/web/bind/annotation/InitBinder.java
  8. 26
      spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java
  9. 11
      spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java
  10. 11
      spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java
  11. 4
      spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java
  12. 4
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java
  13. 11
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java
  14. 95
      src/docs/asciidoc/web/web-data-binding-model-design.adoc
  15. 5
      src/docs/asciidoc/web/webflux.adoc
  16. 7
      src/docs/asciidoc/web/webmvc.adoc

7
spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,8 +23,9 @@ import org.springframework.lang.Nullable; @@ -23,8 +23,9 @@ import org.springframework.lang.Nullable;
/**
* Common interface for classes that can access named properties
* (such as bean properties of an object or fields in an object)
* Serves as base interface for {@link BeanWrapper}.
* (such as bean properties of an object or fields in an object).
*
* <p>Serves as base interface for {@link BeanWrapper}.
*
* @author Juergen Hoeller
* @since 1.1

117
spring-context/src/main/java/org/springframework/validation/DataBinder.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -51,18 +51,20 @@ import org.springframework.util.PatternMatchUtils; @@ -51,18 +51,20 @@ import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils;
/**
* Binder that allows for setting property values onto a target object,
* including support for validation and binding result analysis.
* The binding process can be customized through specifying allowed fields,
* Binder that allows for setting property values on a target object, including
* support for validation and binding result analysis.
*
* <p>The binding process can be customized by specifying allowed field patterns,
* required fields, custom editors, etc.
*
* <p>Note that there are potential security implications in failing to set an array
* of allowed fields. In the case of HTTP form POST data for example, malicious clients
* can attempt to subvert an application by supplying values for fields or properties
* that do not exist on the form. In some cases this could lead to illegal data being
* set on command objects <i>or their nested objects</i>. For this reason, it is
* <b>highly recommended to specify the {@link #setAllowedFields allowedFields} property</b>
* on the DataBinder.
* <p><strong>WARNING</strong>: Data binding can lead to security issues by exposing
* parts of the object graph that are not meant to be accessed or modified by
* external clients. Therefore the design and use of data binding should be considered
* carefully with regard to security. For more details, please refer to the dedicated
* sections on data binding for
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder-model-design">Spring Web MVC</a> and
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-initbinder-model-design">Spring WebFlux</a>
* in the reference manual.
*
* <p>The binding results can be examined via the {@link BindingResult} interface,
* extending the {@link Errors} interface: see the {@link #getBindingResult()} method.
@ -96,6 +98,7 @@ import org.springframework.util.StringUtils; @@ -96,6 +98,7 @@ import org.springframework.util.StringUtils;
* @author Rob Harrop
* @author Stephane Nicoll
* @author Kazuki Shimizu
* @author Sam Brannen
* @see #setAllowedFields
* @see #setRequiredFields
* @see #registerCustomEditor
@ -418,15 +421,21 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -418,15 +421,21 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
/**
* Register fields that should be allowed for binding. Default is all fields.
* Restrict this for example to avoid unwanted modifications by malicious
* Register field patterns that should be allowed for binding.
* <p>Default is all fields.
* <p>Restrict this for example to avoid unwanted modifications by malicious
* users when binding HTTP request parameters.
* <p>Supports "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an
* arbitrary number of pattern parts), as well as direct equality. More
* sophisticated matching can be implemented by overriding the
* {@code isAllowed} method.
* <p>Alternatively, specify a list of <i>disallowed</i> fields.
* @param allowedFields array of field names
* <p>Supports {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as
* well as direct equality.
* <p>The default implementation of this method stores allowed field patterns
* in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) canonical}
* form. Subclasses which override this method must therefore take this into
* account.
* <p>More sophisticated matching can be implemented by overriding the
* {@link #isAllowed} method.
* <p>Alternatively, specify a list of <i>disallowed</i> field patterns.
* @param allowedFields array of allowed field patterns
* @see #setDisallowedFields
* @see #isAllowed(String)
*/
@ -435,8 +444,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -435,8 +444,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
/**
* Return the fields that should be allowed for binding.
* @return array of field names
* Return the field patterns that should be allowed for binding.
* @return array of allowed field patterns
* @see #setAllowedFields(String...)
*/
@Nullable
public String[] getAllowedFields() {
@ -444,25 +454,44 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -444,25 +454,44 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
/**
* Register fields that should <i>not</i> be allowed for binding. Default
* is none. Mark fields as disallowed for example to avoid unwanted
* Register field patterns that should <i>not</i> be allowed for binding.
* <p>Default is none.
* <p>Mark fields as disallowed, for example to avoid unwanted
* modifications by malicious users when binding HTTP request parameters.
* <p>Supports "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an
* arbitrary number of pattern parts), as well as direct equality.
* More sophisticated matching can be implemented by overriding the
* {@code isAllowed} method.
* <p>Alternatively, specify a list of <i>allowed</i> fields.
* @param disallowedFields array of field names
* <p>Supports {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as
* well as direct equality.
* <p>The default implementation of this method stores disallowed field patterns
* in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) canonical}
* form. As of Spring Framework 5.2.21, the default implementation also transforms
* disallowed field patterns to {@linkplain String#toLowerCase() lowercase} to
* support case-insensitive pattern matching in {@link #isAllowed}. Subclasses
* which override this method must therefore take both of these transformations
* into account.
* <p>More sophisticated matching can be implemented by overriding the
* {@link #isAllowed} method.
* <p>Alternatively, specify a list of <i>allowed</i> field patterns.
* @param disallowedFields array of disallowed field patterns
* @see #setAllowedFields
* @see #isAllowed(String)
*/
public void setDisallowedFields(@Nullable String... disallowedFields) {
this.disallowedFields = PropertyAccessorUtils.canonicalPropertyNames(disallowedFields);
if (disallowedFields == null) {
this.disallowedFields = null;
}
else {
String[] fieldPatterns = new String[disallowedFields.length];
for (int i = 0; i < fieldPatterns.length; i++) {
fieldPatterns[i] = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]).toLowerCase();
}
this.disallowedFields = fieldPatterns;
}
}
/**
* Return the fields that should <i>not</i> be allowed for binding.
* @return array of field names
* Return the field patterns that should <i>not</i> be allowed for binding.
* @return array of disallowed field patterns
* @see #setDisallowedFields(String...)
*/
@Nullable
public String[] getDisallowedFields() {
@ -774,16 +803,20 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -774,16 +803,20 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
/**
* Return if the given field is allowed for binding.
* Invoked for each passed-in property value.
* <p>The default implementation checks for "xxx*", "*xxx", "*xxx*" and "xxx*yyy"
* matches (with an arbitrary number of pattern parts), as well as direct equality,
* in the specified lists of allowed fields and disallowed fields. A field matching
* a disallowed pattern will not be accepted even if it also happens to match a
* pattern in the allowed list.
* <p>Can be overridden in subclasses.
* Determine if the given field is allowed for binding.
* <p>Invoked for each passed-in property value.
* <p>Checks for {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as
* well as direct equality, in the configured lists of allowed field patterns
* and disallowed field patterns.
* <p>Matching against allowed field patterns is case-sensitive; whereas,
* matching against disallowed field patterns is case-insensitive.
* <p>A field matching a disallowed pattern will not be accepted even if it
* also happens to match a pattern in the allowed list.
* <p>Can be overridden in subclasses, but care must be taken to honor the
* aforementioned contract.
* @param field the field to check
* @return if the field is allowed
* @return {@code true} if the field is allowed
* @see #setAllowedFields
* @see #setDisallowedFields
* @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
@ -792,7 +825,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -792,7 +825,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
String[] allowed = getAllowedFields();
String[] disallowed = getDisallowedFields();
return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) &&
(ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field)));
(ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field.toLowerCase())));
}
/**

262
spring-context/src/test/java/org/springframework/validation/DataBinderTests.java

@ -64,23 +64,26 @@ import org.springframework.format.support.DefaultFormattingConversionService; @@ -64,23 +64,26 @@ import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.lang.Nullable;
import org.springframework.tests.sample.beans.BeanWithObjectProperty;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.entry;
/**
* Unit tests for {@link DataBinder}.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @author Rob Harrop
* @author Kazuki Shimizu
* @author Sam Brannen
*/
class DataBinderTests {
@Test
void testBindingNoErrors() throws BindException {
void bindingNoErrors() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
assertThat(binder.isIgnoreUnknownFields()).isTrue();
@ -110,12 +113,11 @@ class DataBinderTests { @@ -110,12 +113,11 @@ class DataBinderTests {
assertThat(ex).isEqualTo(binder.getBindingResult());
other.reject("xxx");
boolean condition = !other.equals(binder.getBindingResult());
assertThat(condition).isTrue();
assertThat(other).isNotEqualTo(binder.getBindingResult());
}
@Test
void testBindingWithDefaultConversionNoErrors() throws BindException {
void bindingWithDefaultConversionNoErrors() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
assertThat(binder.isIgnoreUnknownFields()).isTrue();
@ -131,7 +133,7 @@ class DataBinderTests { @@ -131,7 +133,7 @@ class DataBinderTests {
}
@Test
void testNestedBindingWithDefaultConversionNoErrors() throws BindException {
void nestedBindingWithDefaultConversionNoErrors() throws BindException {
TestBean rod = new TestBean(new TestBean());
DataBinder binder = new DataBinder(rod, "person");
assertThat(binder.isIgnoreUnknownFields()).isTrue();
@ -147,7 +149,7 @@ class DataBinderTests { @@ -147,7 +149,7 @@ class DataBinderTests {
}
@Test
void testBindingNoErrorsNotIgnoreUnknown() {
void bindingNoErrorsNotIgnoreUnknown() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
binder.setIgnoreUnknownFields(false);
@ -160,7 +162,7 @@ class DataBinderTests { @@ -160,7 +162,7 @@ class DataBinderTests {
}
@Test
void testBindingNoErrorsWithInvalidField() {
void bindingNoErrorsWithInvalidField() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
@ -171,7 +173,7 @@ class DataBinderTests { @@ -171,7 +173,7 @@ class DataBinderTests {
}
@Test
void testBindingNoErrorsWithIgnoreInvalid() {
void bindingNoErrorsWithIgnoreInvalid() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
binder.setIgnoreInvalidFields(true);
@ -180,10 +182,14 @@ class DataBinderTests { @@ -180,10 +182,14 @@ class DataBinderTests {
pvs.add("spouse.age", 32);
binder.bind(pvs);
binder.close();
assertThat(rod.getName()).isEqualTo("Rod");
assertThat(rod.getSpouse()).isNull();
}
@Test
void testBindingWithErrors() {
void bindingWithErrors() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
@ -245,7 +251,7 @@ class DataBinderTests { @@ -245,7 +251,7 @@ class DataBinderTests {
}
@Test
void testBindingWithSystemFieldError() {
void bindingWithSystemFieldError() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
@ -257,7 +263,7 @@ class DataBinderTests { @@ -257,7 +263,7 @@ class DataBinderTests {
}
@Test
void testBindingWithErrorsAndCustomEditors() {
void bindingWithErrorsAndCustomEditors() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
binder.registerCustomEditor(String.class, "touchy", new PropertyEditorSupport() {
@ -325,7 +331,7 @@ class DataBinderTests { @@ -325,7 +331,7 @@ class DataBinderTests {
}
@Test
void testBindingWithCustomEditorOnObjectField() {
void bindingWithCustomEditorOnObjectField() {
BeanWithObjectProperty tb = new BeanWithObjectProperty();
DataBinder binder = new DataBinder(tb);
binder.registerCustomEditor(Integer.class, "object", new CustomNumberEditor(Integer.class, true));
@ -336,7 +342,7 @@ class DataBinderTests { @@ -336,7 +342,7 @@ class DataBinderTests {
}
@Test
void testBindingWithFormatter() {
void bindingWithFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@ -368,7 +374,7 @@ class DataBinderTests { @@ -368,7 +374,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithFormatter() {
void bindingErrorWithFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@ -391,7 +397,7 @@ class DataBinderTests { @@ -391,7 +397,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithParseExceptionFromFormatter() {
void bindingErrorWithParseExceptionFromFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@ -419,7 +425,7 @@ class DataBinderTests { @@ -419,7 +425,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithRuntimeExceptionFromFormatter() {
void bindingErrorWithRuntimeExceptionFromFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@ -447,7 +453,7 @@ class DataBinderTests { @@ -447,7 +453,7 @@ class DataBinderTests {
}
@Test
void testBindingWithFormatterAgainstList() {
void bindingWithFormatterAgainstList() {
BeanWithIntegerList tb = new BeanWithIntegerList();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@ -469,7 +475,7 @@ class DataBinderTests { @@ -469,7 +475,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithFormatterAgainstList() {
void bindingErrorWithFormatterAgainstList() {
BeanWithIntegerList tb = new BeanWithIntegerList();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@ -492,7 +498,7 @@ class DataBinderTests { @@ -492,7 +498,7 @@ class DataBinderTests {
}
@Test
void testBindingWithFormatterAgainstFields() {
void bindingWithFormatterAgainstFields() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@ -525,7 +531,7 @@ class DataBinderTests { @@ -525,7 +531,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithFormatterAgainstFields() {
void bindingErrorWithFormatterAgainstFields() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
binder.initDirectFieldAccess();
@ -549,7 +555,7 @@ class DataBinderTests { @@ -549,7 +555,7 @@ class DataBinderTests {
}
@Test
void testBindingWithCustomFormatter() {
void bindingWithCustomFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
binder.addCustomFormatter(new NumberStyleFormatter(), Float.class);
@ -578,7 +584,7 @@ class DataBinderTests { @@ -578,7 +584,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithCustomFormatter() {
void bindingErrorWithCustomFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
binder.addCustomFormatter(new NumberStyleFormatter());
@ -599,7 +605,7 @@ class DataBinderTests { @@ -599,7 +605,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithParseExceptionFromCustomFormatter() {
void bindingErrorWithParseExceptionFromCustomFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
@ -624,7 +630,7 @@ class DataBinderTests { @@ -624,7 +630,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithRuntimeExceptionFromCustomFormatter() {
void bindingErrorWithRuntimeExceptionFromCustomFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
@ -649,7 +655,7 @@ class DataBinderTests { @@ -649,7 +655,7 @@ class DataBinderTests {
}
@Test
void testConversionWithInappropriateStringEditor() {
void conversionWithInappropriateStringEditor() {
DataBinder dataBinder = new DataBinder(null);
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
dataBinder.setConversionService(conversionService);
@ -662,7 +668,7 @@ class DataBinderTests { @@ -662,7 +668,7 @@ class DataBinderTests {
}
@Test
void testBindingWithAllowedFields() throws BindException {
void bindingWithAllowedFields() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
binder.setAllowedFields("name", "myparam");
@ -672,30 +678,32 @@ class DataBinderTests { @@ -672,30 +678,32 @@ class DataBinderTests {
binder.bind(pvs);
binder.close();
assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue();
assertThat(rod.getAge() == 0).as("did not change age").isTrue();
assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod");
assertThat(rod.getAge()).as("did not change age").isZero();
}
@Test
void testBindingWithDisallowedFields() throws BindException {
void bindingWithDisallowedFields() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
binder.setDisallowedFields("age");
binder.setDisallowedFields(" ", "\t", "favouriteColour", null, "age");
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("name", "Rod");
pvs.add("age", "32x");
pvs.add("favouriteColour", "BLUE");
binder.bind(pvs);
binder.close();
assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue();
assertThat(rod.getAge() == 0).as("did not change age").isTrue();
String[] disallowedFields = binder.getBindingResult().getSuppressedFields();
assertThat(disallowedFields.length).isEqualTo(1);
assertThat(disallowedFields[0]).isEqualTo("age");
assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod");
assertThat(rod.getAge()).as("did not change age").isZero();
assertThat(rod.getFavouriteColour()).as("did not change favourite colour").isNull();
assertThat(binder.getBindingResult().getSuppressedFields()).containsExactlyInAnyOrder("age", "favouriteColour");
}
@Test
void testBindingWithAllowedAndDisallowedFields() throws BindException {
void bindingWithAllowedAndDisallowedFields() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
binder.setAllowedFields("name", "myparam");
@ -706,34 +714,32 @@ class DataBinderTests { @@ -706,34 +714,32 @@ class DataBinderTests {
binder.bind(pvs);
binder.close();
assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue();
assertThat(rod.getAge() == 0).as("did not change age").isTrue();
String[] disallowedFields = binder.getBindingResult().getSuppressedFields();
assertThat(disallowedFields).hasSize(1);
assertThat(disallowedFields[0]).isEqualTo("age");
assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod");
assertThat(rod.getAge()).as("did not change age").isZero();
assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("age");
}
@Test
void testBindingWithOverlappingAllowedAndDisallowedFields() throws BindException {
void bindingWithOverlappingAllowedAndDisallowedFields() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
binder.setAllowedFields("name", "age");
binder.setDisallowedFields("age");
binder.setDisallowedFields("AGE");
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("name", "Rod");
pvs.add("age", "32x");
binder.bind(pvs);
binder.close();
assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue();
assertThat(rod.getAge() == 0).as("did not change age").isTrue();
String[] disallowedFields = binder.getBindingResult().getSuppressedFields();
assertThat(disallowedFields).hasSize(1);
assertThat(disallowedFields[0]).isEqualTo("age");
assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod");
assertThat(rod.getAge()).as("did not change age").isZero();
assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("age");
}
@Test
void testBindingWithAllowedFieldsUsingAsterisks() throws BindException {
void bindingWithAllowedFieldsUsingAsterisks() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
binder.setAllowedFields("nam*", "*ouchy");
@ -760,11 +766,11 @@ class DataBinderTests { @@ -760,11 +766,11 @@ class DataBinderTests {
}
@Test
void testBindingWithAllowedAndDisallowedMapFields() throws BindException {
void bindingWithAllowedAndDisallowedMapFields() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
binder.setAllowedFields("someMap[key1]", "someMap[key2]");
binder.setDisallowedFields("someMap['key3']", "someMap[key4]");
binder.setDisallowedFields("someMap['KEY3']", "SomeMap[key4]");
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("someMap[key1]", "value1");
@ -774,21 +780,18 @@ class DataBinderTests { @@ -774,21 +780,18 @@ class DataBinderTests {
binder.bind(pvs);
binder.close();
assertThat(rod.getSomeMap().get("key1")).isEqualTo("value1");
assertThat(rod.getSomeMap().get("key2")).isEqualTo("value2");
assertThat(rod.getSomeMap().get("key3")).isNull();
assertThat(rod.getSomeMap().get("key4")).isNull();
String[] disallowedFields = binder.getBindingResult().getSuppressedFields();
assertThat(disallowedFields).hasSize(2);
assertThat(ObjectUtils.containsElement(disallowedFields, "someMap[key3]")).isTrue();
assertThat(ObjectUtils.containsElement(disallowedFields, "someMap[key4]")).isTrue();
@SuppressWarnings("unchecked")
Map<String, String> someMap = (Map<String, String>) rod.getSomeMap();
assertThat(someMap).containsOnly(entry("key1", "value1"), entry("key2", "value2"));
assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("someMap[key3]", "someMap[key4]");
}
/**
* Tests for required field, both null, non-existing and empty strings.
*/
@Test
void testBindingWithRequiredFields() {
void bindingWithRequiredFields() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
@ -819,7 +822,7 @@ class DataBinderTests { @@ -819,7 +822,7 @@ class DataBinderTests {
}
@Test
void testBindingWithRequiredMapFields() {
void bindingWithRequiredMapFields() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
@ -839,7 +842,7 @@ class DataBinderTests { @@ -839,7 +842,7 @@ class DataBinderTests {
}
@Test
void testBindingWithNestedObjectCreation() {
void bindingWithNestedObjectCreation() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "person");
@ -860,7 +863,7 @@ class DataBinderTests { @@ -860,7 +863,7 @@ class DataBinderTests {
}
@Test
void testCustomEditorWithOldValueAccess() {
void customEditorWithOldValueAccess() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
@ -885,7 +888,7 @@ class DataBinderTests { @@ -885,7 +888,7 @@ class DataBinderTests {
}
@Test
void testCustomEditorForSingleProperty() {
void customEditorForSingleProperty() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
DataBinder binder = new DataBinder(tb, "tb");
@ -925,7 +928,7 @@ class DataBinderTests { @@ -925,7 +928,7 @@ class DataBinderTests {
}
@Test
void testCustomEditorForPrimitiveProperty() {
void customEditorForPrimitiveProperty() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
@ -949,7 +952,7 @@ class DataBinderTests { @@ -949,7 +952,7 @@ class DataBinderTests {
}
@Test
void testCustomEditorForAllStringProperties() {
void customEditorForAllStringProperties() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
@ -981,7 +984,7 @@ class DataBinderTests { @@ -981,7 +984,7 @@ class DataBinderTests {
}
@Test
void testCustomFormatterForSingleProperty() {
void customFormatterForSingleProperty() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
DataBinder binder = new DataBinder(tb, "tb");
@ -1021,7 +1024,7 @@ class DataBinderTests { @@ -1021,7 +1024,7 @@ class DataBinderTests {
}
@Test
void testCustomFormatterForPrimitiveProperty() {
void customFormatterForPrimitiveProperty() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
@ -1045,7 +1048,7 @@ class DataBinderTests { @@ -1045,7 +1048,7 @@ class DataBinderTests {
}
@Test
void testCustomFormatterForAllStringProperties() {
void customFormatterForAllStringProperties() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
@ -1077,7 +1080,7 @@ class DataBinderTests { @@ -1077,7 +1080,7 @@ class DataBinderTests {
}
@Test
void testJavaBeanPropertyConventions() {
void javaBeanPropertyConventions() {
Book book = new Book();
DataBinder binder = new DataBinder(book);
@ -1101,7 +1104,7 @@ class DataBinderTests { @@ -1101,7 +1104,7 @@ class DataBinderTests {
}
@Test
void testOptionalProperty() {
void optionalProperty() {
OptionalHolder bean = new OptionalHolder();
DataBinder binder = new DataBinder(bean);
binder.setConversionService(new DefaultConversionService());
@ -1122,7 +1125,7 @@ class DataBinderTests { @@ -1122,7 +1125,7 @@ class DataBinderTests {
}
@Test
void testValidatorNoErrors() throws Exception {
void validatorNoErrors() throws Exception {
TestBean tb = new TestBean();
tb.setAge(33);
tb.setName("Rod");
@ -1175,15 +1178,13 @@ class DataBinderTests { @@ -1175,15 +1178,13 @@ class DataBinderTests {
assertThat(errors.getNestedPath()).isEqualTo("spouse.");
assertThat(errors.getErrorCount()).isEqualTo(1);
boolean condition1 = !errors.hasGlobalErrors();
assertThat(condition1).isTrue();
assertThat(errors.hasGlobalErrors()).isFalse();
assertThat(errors.getFieldErrorCount("age")).isEqualTo(1);
boolean condition = !errors.hasFieldErrors("name");
assertThat(condition).isTrue();
assertThat(errors.hasFieldErrors("name")).isFalse();
}
@Test
void testValidatorWithErrors() {
void validatorWithErrors() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
@ -1252,7 +1253,7 @@ class DataBinderTests { @@ -1252,7 +1253,7 @@ class DataBinderTests {
}
@Test
void testValidatorWithErrorsAndCodesPrefix() {
void validatorWithErrorsAndCodesPrefix() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
@ -1324,7 +1325,7 @@ class DataBinderTests { @@ -1324,7 +1325,7 @@ class DataBinderTests {
}
@Test
void testValidatorWithNestedObjectNull() {
void validatorWithNestedObjectNull() {
TestBean tb = new TestBean();
Errors errors = new BeanPropertyBindingResult(tb, "tb");
Validator testValidator = new TestBeanValidator();
@ -1343,7 +1344,7 @@ class DataBinderTests { @@ -1343,7 +1344,7 @@ class DataBinderTests {
}
@Test
void testNestedValidatorWithoutNestedPath() {
void nestedValidatorWithoutNestedPath() {
TestBean tb = new TestBean();
tb.setName("XXX");
Errors errors = new BeanPropertyBindingResult(tb, "tb");
@ -1357,7 +1358,8 @@ class DataBinderTests { @@ -1357,7 +1358,8 @@ class DataBinderTests {
}
@Test
void testBindingStringArrayToIntegerSet() {
@SuppressWarnings("unchecked")
void bindingStringArrayToIntegerSet() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(Set.class, new CustomCollectionEditor(TreeSet.class) {
@ -1371,12 +1373,8 @@ class DataBinderTests { @@ -1371,12 +1373,8 @@ class DataBinderTests {
binder.bind(pvs);
assertThat(binder.getBindingResult().getFieldValue("set")).isEqualTo(tb.getSet());
boolean condition = tb.getSet() instanceof TreeSet;
assertThat(condition).isTrue();
assertThat(tb.getSet().size()).isEqualTo(3);
assertThat(tb.getSet().contains(10)).isTrue();
assertThat(tb.getSet().contains(20)).isTrue();
assertThat(tb.getSet().contains(30)).isTrue();
assertThat(tb.getSet()).isInstanceOf(TreeSet.class);
assertThat((Set<Integer>) tb.getSet()).containsExactly(10, 20, 30);
pvs = new MutablePropertyValues();
pvs.add("set", null);
@ -1386,7 +1384,7 @@ class DataBinderTests { @@ -1386,7 +1384,7 @@ class DataBinderTests {
}
@Test
void testBindingNullToEmptyCollection() {
void bindingNullToEmptyCollection() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(Set.class, new CustomCollectionEditor(TreeSet.class, true));
@ -1394,13 +1392,12 @@ class DataBinderTests { @@ -1394,13 +1392,12 @@ class DataBinderTests {
pvs.add("set", null);
binder.bind(pvs);
boolean condition = tb.getSet() instanceof TreeSet;
assertThat(condition).isTrue();
assertThat(tb.getSet().isEmpty()).isTrue();
assertThat(tb.getSet()).isInstanceOf(TreeSet.class);
assertThat(tb.getSet()).isEmpty();
}
@Test
void testBindingToIndexedField() {
void bindingToIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(String.class, "array.name", new PropertyEditorSupport() {
@ -1439,7 +1436,7 @@ class DataBinderTests { @@ -1439,7 +1436,7 @@ class DataBinderTests {
}
@Test
void testBindingToNestedIndexedField() {
void bindingToNestedIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean());
tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean());
@ -1470,7 +1467,7 @@ class DataBinderTests { @@ -1470,7 +1467,7 @@ class DataBinderTests {
}
@Test
void testEditorForNestedIndexedField() {
void editorForNestedIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean());
tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean());
@ -1496,7 +1493,7 @@ class DataBinderTests { @@ -1496,7 +1493,7 @@ class DataBinderTests {
}
@Test
void testSpecificEditorForNestedIndexedField() {
void specificEditorForNestedIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean());
tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean());
@ -1522,7 +1519,7 @@ class DataBinderTests { @@ -1522,7 +1519,7 @@ class DataBinderTests {
}
@Test
void testInnerSpecificEditorForNestedIndexedField() {
void innerSpecificEditorForNestedIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean());
tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean());
@ -1548,7 +1545,7 @@ class DataBinderTests { @@ -1548,7 +1545,7 @@ class DataBinderTests {
}
@Test
void testDirectBindingToIndexedField() {
void directBindingToIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(TestBean.class, "array", new PropertyEditorSupport() {
@ -1601,7 +1598,7 @@ class DataBinderTests { @@ -1601,7 +1598,7 @@ class DataBinderTests {
}
@Test
void testDirectBindingToEmptyIndexedFieldWithRegisteredSpecificEditor() {
void directBindingToEmptyIndexedFieldWithRegisteredSpecificEditor() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(TestBean.class, "map[key0]", new PropertyEditorSupport() {
@ -1632,7 +1629,7 @@ class DataBinderTests { @@ -1632,7 +1629,7 @@ class DataBinderTests {
}
@Test
void testDirectBindingToEmptyIndexedFieldWithRegisteredGenericEditor() {
void directBindingToEmptyIndexedFieldWithRegisteredGenericEditor() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(TestBean.class, "map", new PropertyEditorSupport() {
@ -1663,7 +1660,7 @@ class DataBinderTests { @@ -1663,7 +1660,7 @@ class DataBinderTests {
}
@Test
void testCustomEditorWithSubclass() {
void customEditorWithSubclass() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(TestBean.class, new PropertyEditorSupport() {
@ -1697,7 +1694,7 @@ class DataBinderTests { @@ -1697,7 +1694,7 @@ class DataBinderTests {
}
@Test
void testBindToStringArrayWithArrayEditor() {
void bindToStringArrayWithArrayEditor() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(String[].class, "stringArray", new PropertyEditorSupport() {
@ -1709,15 +1706,12 @@ class DataBinderTests { @@ -1709,15 +1706,12 @@ class DataBinderTests {
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("stringArray", "a1-b2");
binder.bind(pvs);
boolean condition = !binder.getBindingResult().hasErrors();
assertThat(condition).isTrue();
assertThat(tb.getStringArray().length).isEqualTo(2);
assertThat(tb.getStringArray()[0]).isEqualTo("a1");
assertThat(tb.getStringArray()[1]).isEqualTo("b2");
assertThat(binder.getBindingResult().hasErrors()).isFalse();
assertThat(tb.getStringArray()).containsExactly("a1", "b2");
}
@Test
void testBindToStringArrayWithComponentEditor() {
void bindToStringArrayWithComponentEditor() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(String.class, "stringArray", new PropertyEditorSupport() {
@ -1729,15 +1723,14 @@ class DataBinderTests { @@ -1729,15 +1723,14 @@ class DataBinderTests {
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("stringArray", new String[] {"a1", "b2"});
binder.bind(pvs);
boolean condition = !binder.getBindingResult().hasErrors();
assertThat(condition).isTrue();
assertThat(binder.getBindingResult().hasErrors()).isFalse();
assertThat(tb.getStringArray().length).isEqualTo(2);
assertThat(tb.getStringArray()[0]).isEqualTo("Xa1");
assertThat(tb.getStringArray()[1]).isEqualTo("Xb2");
}
@Test
void testBindingErrors() {
void bindingErrors() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
@ -1764,7 +1757,7 @@ class DataBinderTests { @@ -1764,7 +1757,7 @@ class DataBinderTests {
}
@Test
void testAddAllErrors() {
void addAllErrors() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
@ -1784,7 +1777,7 @@ class DataBinderTests { @@ -1784,7 +1777,7 @@ class DataBinderTests {
@Test
@SuppressWarnings("unchecked")
void testBindingWithResortedList() {
void bindingWithResortedList() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
MutablePropertyValues pvs = new MutablePropertyValues();
@ -1802,7 +1795,7 @@ class DataBinderTests { @@ -1802,7 +1795,7 @@ class DataBinderTests {
}
@Test
void testRejectWithoutDefaultMessage() {
void rejectWithoutDefaultMessage() {
TestBean tb = new TestBean();
tb.setName("myName");
tb.setAge(99);
@ -1820,7 +1813,7 @@ class DataBinderTests { @@ -1820,7 +1813,7 @@ class DataBinderTests {
}
@Test
void testBindExceptionSerializable() throws Exception {
void bindExceptionSerializable() throws Exception {
SerializablePerson tb = new SerializablePerson();
tb.setName("myName");
tb.setAge(99);
@ -1849,27 +1842,27 @@ class DataBinderTests { @@ -1849,27 +1842,27 @@ class DataBinderTests {
}
@Test
void testTrackDisallowedFields() {
void trackDisallowedFields() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.setAllowedFields("name", "age");
String name = "Rob Harrop";
String beanName = "foobar";
int age = 42;
MutablePropertyValues mpvs = new MutablePropertyValues();
mpvs.add("name", name);
mpvs.add("beanName", beanName);
mpvs.add("age", age);
mpvs.add("beanName", "foobar");
binder.bind(mpvs);
assertThat(testBean.getName()).isEqualTo(name);
String[] disallowedFields = binder.getBindingResult().getSuppressedFields();
assertThat(disallowedFields).hasSize(1);
assertThat(disallowedFields[0]).isEqualTo("beanName");
assertThat(testBean.getAge()).isEqualTo(age);
assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("beanName");
}
@Test
void testAutoGrowWithinDefaultLimit() {
void autoGrowWithinDefaultLimit() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
@ -1881,7 +1874,7 @@ class DataBinderTests { @@ -1881,7 +1874,7 @@ class DataBinderTests {
}
@Test
void testAutoGrowBeyondDefaultLimit() {
void autoGrowBeyondDefaultLimit() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
@ -1894,7 +1887,7 @@ class DataBinderTests { @@ -1894,7 +1887,7 @@ class DataBinderTests {
}
@Test
void testAutoGrowWithinCustomLimit() {
void autoGrowWithinCustomLimit() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.setAutoGrowCollectionLimit(10);
@ -1907,7 +1900,7 @@ class DataBinderTests { @@ -1907,7 +1900,7 @@ class DataBinderTests {
}
@Test
void testAutoGrowBeyondCustomLimit() {
void autoGrowBeyondCustomLimit() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.setAutoGrowCollectionLimit(10);
@ -1921,7 +1914,7 @@ class DataBinderTests { @@ -1921,7 +1914,7 @@ class DataBinderTests {
}
@Test
void testNestedGrowingList() {
void nestedGrowingList() {
Form form = new Form();
DataBinder binder = new DataBinder(form, "form");
MutablePropertyValues mpv = new MutablePropertyValues();
@ -1937,7 +1930,7 @@ class DataBinderTests { @@ -1937,7 +1930,7 @@ class DataBinderTests {
}
@Test
void testFieldErrorAccessVariations() {
void fieldErrorAccessVariations() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
assertThat(binder.getBindingResult().getGlobalError()).isNull();
@ -1958,7 +1951,7 @@ class DataBinderTests { @@ -1958,7 +1951,7 @@ class DataBinderTests {
}
@Test // SPR-14888
void testSetAutoGrowCollectionLimit() {
void setAutoGrowCollectionLimit() {
BeanWithIntegerList tb = new BeanWithIntegerList();
DataBinder binder = new DataBinder(tb);
binder.setAutoGrowCollectionLimit(257);
@ -1972,7 +1965,7 @@ class DataBinderTests { @@ -1972,7 +1965,7 @@ class DataBinderTests {
}
@Test // SPR-14888
void testSetAutoGrowCollectionLimitAfterInitialization() {
void setAutoGrowCollectionLimitAfterInitialization() {
DataBinder binder = new DataBinder(new BeanWithIntegerList());
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
assertThatIllegalStateException().isThrownBy(() ->
@ -1981,7 +1974,7 @@ class DataBinderTests { @@ -1981,7 +1974,7 @@ class DataBinderTests {
}
@Test // SPR-15009
void testSetCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAccess() {
void setCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAccess() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver();
@ -1998,7 +1991,7 @@ class DataBinderTests { @@ -1998,7 +1991,7 @@ class DataBinderTests {
}
@Test // SPR-15009
void testSetCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFieldAccess() {
void setCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFieldAccess() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver();
@ -2013,7 +2006,7 @@ class DataBinderTests { @@ -2013,7 +2006,7 @@ class DataBinderTests {
}
@Test // SPR-15009
void testSetCustomMessageCodesResolverAfterInitializeBindingResult() {
void setCustomMessageCodesResolverAfterInitializeBindingResult() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.initBeanPropertyAccess();
@ -2028,7 +2021,7 @@ class DataBinderTests { @@ -2028,7 +2021,7 @@ class DataBinderTests {
}
@Test // SPR-15009
void testSetMessageCodesResolverIsNullAfterInitializeBindingResult() {
void setMessageCodesResolverIsNullAfterInitializeBindingResult() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.initBeanPropertyAccess();
@ -2042,8 +2035,7 @@ class DataBinderTests { @@ -2042,8 +2035,7 @@ class DataBinderTests {
}
@Test // SPR-15009
void testCallSetMessageCodesResolverTwice() {
void callSetMessageCodesResolverTwice() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.setMessageCodesResolver(new DefaultMessageCodesResolver());

11
spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -33,6 +33,15 @@ import org.springframework.web.util.WebUtils; @@ -33,6 +33,15 @@ import org.springframework.web.util.WebUtils;
* Special {@link org.springframework.validation.DataBinder} to perform data binding
* from servlet request parameters to JavaBeans, including support for multipart files.
*
* <p><strong>WARNING</strong>: Data binding can lead to security issues by exposing
* parts of the object graph that are not meant to be accessed or modified by
* external clients. Therefore the design and use of data binding should be considered
* carefully with regard to security. For more details, please refer to the dedicated
* sections on data binding for
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder-model-design">Spring Web MVC</a> and
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-initbinder-model-design">Spring WebFlux</a>
* in the reference manual.
*
* <p>See the DataBinder/WebDataBinder superclasses for customization options,
* which include specifying allowed/required fields, and registering custom
* property editors.

11
spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -34,6 +34,15 @@ import org.springframework.web.multipart.MultipartFile; @@ -34,6 +34,15 @@ import org.springframework.web.multipart.MultipartFile;
* the Servlet API; serves as base class for more specific DataBinder variants,
* such as {@link org.springframework.web.bind.ServletRequestDataBinder}.
*
* <p><strong>WARNING</strong>: Data binding can lead to security issues by exposing
* parts of the object graph that are not meant to be accessed or modified by
* external clients. Therefore the design and use of data binding should be considered
* carefully with regard to security. For more details, please refer to the dedicated
* sections on data binding for
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder-model-design">Spring Web MVC</a> and
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-initbinder-model-design">Spring WebFlux</a>
* in the reference manual.
*
* <p>Includes support for field markers which address a common problem with
* HTML checkboxes and select options: detecting that a field was part of
* the form, but did not generate a request parameter because it was empty.

1
spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java

@ -100,6 +100,7 @@ import java.lang.annotation.Target; @@ -100,6 +100,7 @@ import java.lang.annotation.Target;
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 3.0
* @see ControllerAdvice
* @see org.springframework.web.context.request.WebRequest
*/
@Target(ElementType.METHOD)

22
spring-web/src/main/java/org/springframework/web/bind/annotation/InitBinder.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,15 +23,24 @@ import java.lang.annotation.RetentionPolicy; @@ -23,15 +23,24 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation that identifies methods which initialize the
* Annotation that identifies methods that initialize the
* {@link org.springframework.web.bind.WebDataBinder} which
* will be used for populating command and form object arguments
* of annotated handler methods.
*
* <p>Such init-binder methods support all arguments that {@link RequestMapping}
* supports, except for command/form objects and corresponding validation result
* objects. Init-binder methods must not have a return value; they are usually
* declared as {@code void}.
* <p><strong>WARNING</strong>: Data binding can lead to security issues by exposing
* parts of the object graph that are not meant to be accessed or modified by
* external clients. Therefore the design and use of data binding should be considered
* carefully with regard to security. For more details, please refer to the dedicated
* sections on data binding for
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder-model-design">Spring Web MVC</a> and
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-initbinder-model-design">Spring WebFlux</a>
* in the reference manual.
*
* <p>{@code @InitBinder} methods support all arguments that
* {@link RequestMapping @RequestMapping} methods support, except for command/form
* objects and corresponding validation result objects. {@code @InitBinder} methods
* must not have a return value; they are usually declared as {@code void}.
*
* <p>Typical arguments are {@link org.springframework.web.bind.WebDataBinder}
* in combination with {@link org.springframework.web.context.request.WebRequest}
@ -39,6 +48,7 @@ import java.lang.annotation.Target; @@ -39,6 +48,7 @@ import java.lang.annotation.Target;
*
* @author Juergen Hoeller
* @since 2.5
* @see ControllerAdvice
* @see org.springframework.web.bind.WebDataBinder
* @see org.springframework.web.context.request.WebRequest
*/

26
spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -31,18 +31,27 @@ import org.springframework.ui.Model; @@ -31,18 +31,27 @@ import org.springframework.ui.Model;
* for controller classes with {@link RequestMapping @RequestMapping}
* methods.
*
* <p>Can be used to expose command objects to a web view, using
* specific attribute names, through annotating corresponding
* parameters of an {@link RequestMapping @RequestMapping} method.
* <p><strong>WARNING</strong>: Data binding can lead to security issues by exposing
* parts of the object graph that are not meant to be accessed or modified by
* external clients. Therefore the design and use of data binding should be considered
* carefully with regard to security. For more details, please refer to the dedicated
* sections on data binding for
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder-model-design">Spring Web MVC</a> and
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-initbinder-model-design">Spring WebFlux</a>
* in the reference manual.
*
* <p>Can also be used to expose reference data to a web view
* through annotating accessor methods in a controller class with
* <p>{@code @ModelAttribute} can be used to expose command objects to a web view,
* using specific attribute names, by annotating corresponding parameters of an
* {@link RequestMapping @RequestMapping} method.
*
* <p>{@code @ModelAttribute} can also be used to expose reference data to a web
* view by annotating accessor methods in a controller class with
* {@link RequestMapping @RequestMapping} methods. Such accessor
* methods are allowed to have any arguments that
* {@link RequestMapping @RequestMapping} methods support, returning
* the model attribute value to expose.
*
* <p>Note however that reference data and all other model content is
* <p>Note however that reference data and all other model content are
* not available to web views when request processing results in an
* {@code Exception} since the exception could be raised at any time
* making the content of the model unreliable. For this reason
@ -52,6 +61,7 @@ import org.springframework.ui.Model; @@ -52,6 +61,7 @@ import org.springframework.ui.Model;
* @author Juergen Hoeller
* @author Rossen Stoyanchev
* @since 2.5
* @see ControllerAdvice
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ -77,7 +87,7 @@ public @interface ModelAttribute { @@ -77,7 +87,7 @@ public @interface ModelAttribute {
String name() default "";
/**
* Allows declaring data binding disabled directly on an {@code @ModelAttribute}
* Allows data binding to be disabled directly on an {@code @ModelAttribute}
* method parameter or on the attribute returned from an {@code @ModelAttribute}
* method, both of which would prevent data binding for that attribute.
* <p>By default this is set to {@code true} in which case data binding applies.

11
spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -36,6 +36,15 @@ import org.springframework.web.server.ServerWebExchange; @@ -36,6 +36,15 @@ import org.springframework.web.server.ServerWebExchange;
* Specialized {@link org.springframework.validation.DataBinder} to perform data
* binding from URL query parameters or form data in the request data to Java objects.
*
* <p><strong>WARNING</strong>: Data binding can lead to security issues by exposing
* parts of the object graph that are not meant to be accessed or modified by
* external clients. Therefore the design and use of data binding should be considered
* carefully with regard to security. For more details, please refer to the dedicated
* sections on data binding for
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder-model-design">Spring Web MVC</a> and
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-initbinder-model-design">Spring WebFlux</a>
* in the reference manual.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 5.0

11
spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -35,6 +35,15 @@ import org.springframework.web.multipart.support.StandardServletPartUtils; @@ -35,6 +35,15 @@ import org.springframework.web.multipart.support.StandardServletPartUtils;
* Special {@link org.springframework.validation.DataBinder} to perform data binding
* from web request parameters to JavaBeans, including support for multipart files.
*
* <p><strong>WARNING</strong>: Data binding can lead to security issues by exposing
* parts of the object graph that are not meant to be accessed or modified by
* external clients. Therefore the design and use of data binding should be considered
* carefully with regard to security. For more details, please refer to the dedicated
* sections on data binding for
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder-model-design">Spring Web MVC</a> and
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-initbinder-model-design">Spring WebFlux</a>
* in the reference manual.
*
* <p>See the DataBinder/WebDataBinder superclasses for customization options,
* which include specifying allowed/required fields, and registering custom
* property editors.

4
spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -116,7 +116,7 @@ public class InitBinderDataBinderFactoryTests { @@ -116,7 +116,7 @@ public class InitBinderDataBinderFactoryTests {
WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, "foo");
assertThat(dataBinder.getDisallowedFields()).isNotNull();
assertThat(dataBinder.getDisallowedFields()[0]).isEqualTo("requestParam-22");
assertThat(dataBinder.getDisallowedFields()[0]).isEqualToIgnoringCase("requestParam-22");
}
private WebDataBinderFactory createFactory(String methodName, Class<?>... parameterTypes)

4
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -121,7 +121,7 @@ public class InitBinderBindingContextTests { @@ -121,7 +121,7 @@ public class InitBinderBindingContextTests {
WebDataBinder dataBinder = context.createDataBinder(exchange, null, "foo");
assertThat(dataBinder.getDisallowedFields()).isNotNull();
assertThat(dataBinder.getDisallowedFields()[0]).isEqualTo("requestParam-22");
assertThat(dataBinder.getDisallowedFields()[0]).isEqualToIgnoringCase("requestParam-22");
}

11
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -29,6 +29,15 @@ import org.springframework.web.servlet.HandlerMapping; @@ -29,6 +29,15 @@ import org.springframework.web.servlet.HandlerMapping;
* Subclass of {@link ServletRequestDataBinder} that adds URI template variables
* to the values used for data binding.
*
* <p><strong>WARNING</strong>: Data binding can lead to security issues by exposing
* parts of the object graph that are not meant to be accessed or modified by
* external clients. Therefore the design and use of data binding should be considered
* carefully with regard to security. For more details, please refer to the dedicated
* sections on data binding for
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder-model-design">Spring Web MVC</a> and
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-initbinder-model-design">Spring WebFlux</a>
* in the reference manual.
*
* @author Rossen Stoyanchev
* @since 3.1
* @see ServletRequestDataBinder

95
src/docs/asciidoc/web/web-data-binding-model-design.adoc

@ -0,0 +1,95 @@ @@ -0,0 +1,95 @@
In the context of web applications, _data binding_ involves the binding of HTTP request
parameters (that is, form data or query parameters) to properties in a model object and
its nested objects.
Only `public` properties following the
https://www.oracle.com/java/technologies/javase/javabeans-spec.html[JavaBeans naming conventions]
are exposed for data binding — for example, `public String getFirstName()` and
`public void setFirstName(String)` methods for a `firstName` property.
TIP: The model object, and its nested object graph, is also sometimes referred to as a
_command object_, _form-backing object_, or _POJO_ (Plain Old Java Object).
By default, Spring permits binding to all public properties in the model object graph.
This means you need to carefully consider what public properties the model has, since a
client could target any public property path, even some that are not expected to be
targeted for a given use case.
For example, given an HTTP form data endpoint, a malicious client could supply values for
properties that exist in the model object graph but are not part of the HTML form
presented in the browser. This could lead to data being set on the model object and any
of its nested objects, that is not expected to be updated.
The recommended approach is to use a _dedicated model object_ that exposes only
properties that are relevant for the form submission. For example, on a form for changing
a user's email address, the model object should declare a minimum set of properties such
as in the following `ChangeEmailForm`.
[source,java,indent=0,subs="verbatim,quotes"]
----
public class ChangeEmailForm {
private String oldEmailAddress;
private String newEmailAddress;
public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}
public String getOldEmailAddress() {
return this.oldEmailAddress;
}
public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}
public String getNewEmailAddress() {
return this.newEmailAddress;
}
}
----
If you cannot or do not want to use a _dedicated model object_ for each data
binding use case, you **must** limit the properties that are allowed for data binding.
Ideally, you can achieve this by registering _allowed field patterns_ via the
`setAllowedFields()` method on `WebDataBinder`.
For example, to register allowed field patterns in your application, you can implement an
`@InitBinder` method in a `@Controller` or `@ControllerAdvice` component as shown below:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
----
In addition to registering allowed patterns, it is also possible to register _disallowed
field patterns_ via the `setDisallowedFields()` method in `DataBinder` and its subclasses.
Please note, however, that an "allow list" is safer than a "deny list". Consequently,
`setAllowedFields()` should be favored over `setDisallowedFields()`.
Note that matching against allowed field patterns is case-sensitive; whereas, matching
against disallowed field patterns is case-insensitive. In addition, a field matching a
disallowed pattern will not be accepted even if it also happens to match a pattern in the
allowed list.
[WARNING]
====
It is extremely important to properly configure allowed and disallowed field patterns
when exposing your domain model directly for data binding purposes. Otherwise, it is a
big security risk.
Furthermore, it is strongly recommended that you do **not** use types from your domain
model such as JPA or Hibernate entities as the model object in data binding scenarios.
====

5
src/docs/asciidoc/web/webflux.adoc

@ -3319,6 +3319,11 @@ controller-specific `Formatter` instances, as the following example shows: @@ -3319,6 +3319,11 @@ controller-specific `Formatter` instances, as the following example shows:
----
<1> Adding a custom formatter (a `DateFormatter`, in this case).
[[webflux-ann-initbinder-model-design]]
==== Model Design
[.small]#<<web.adoc#mvc-ann-initbinder-model-design, Web MVC>>#
include::web-data-binding-model-design.adoc[]
[[webflux-ann-controller-exceptions]]

7
src/docs/asciidoc/web/webmvc.adoc

@ -3751,6 +3751,13 @@ controller-specific `Formatter` implementations, as the following example shows: @@ -3751,6 +3751,13 @@ controller-specific `Formatter` implementations, as the following example shows:
----
<1> Defining an `@InitBinder` method on a custom formatter.
[[mvc-ann-initbinder-model-design]]
==== Model Design
[.small]#<<web-reactive.adoc#webflux-ann-initbinder-model-design, WebFlux>>#
include::web-data-binding-model-design.adoc[]
[[mvc-ann-exceptionhandler]]
=== Exceptions
[.small]#<<web-reactive.adoc#webflux-ann-controller-exceptions, WebFlux>>#

Loading…
Cancel
Save