Software Securityhome
Fix a findingSearch
Fix card · A08:2025 Software or Data Integrity Failures

Fix mass assignment (over-posting) in Java and Spring Boot

The vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.

In short

Bind @RequestBody to a record that declares only the fields users may edit (here ProfileUpdate(displayName, bio)), never to the entity, and copy those fields onto the entity yourself. Do not use readerForUpdating(entity) or BeanUtils.copyProperties on request data. Jackson 3 ignores undeclared JSON properties by default, so role and tenantId never reach the entity.

Stack
  • Language Java 21
  • Runtime OpenJDK 21.0.12.1 (Homebrew), Maven 3.9.16
  • Framework Spring Boot 4.1.1 (Spring Framework 7.0.9, Spring MVC, Jackson 3)
Maps to
Verified
Tests pass · fixed, vulnerable and wrong runs as expected
Other stacks9 cards
Download the project ZIP, 20 KB
On this page

Steps

  1. Find every handler that binds request data onto an entity: @RequestBody or @ModelAttribute parameters typed as an entity, objectMapper.readerForUpdating(entity) or updateValue(entity, ...), BeanUtils.copyProperties(input, entity), MapStruct mappings into the entity and Spring Data REST repositories exposed for PUT and PATCH.
  2. For each action, declare a record with only the fields that caller may change, here record ProfileUpdate(String displayName, String bio) for PATCH /me, and bind @RequestBody to it. Give the admin screen its own record and endpoint rather than adding role to this one.
  3. Copy the record's fields onto the entity one by one (if (changes.bio() != null) user.setBio(changes.bio())), so a column added to the entity never becomes writable by accident.
  4. Decide about undeclared JSON properties: Jackson 3 ignores them by default. To refuse them with 400 instead, set spring.jackson.deserialization.fail-on-unknown-properties=true, which applies to every endpoint.
  5. Set role, tenantId, ownerId and similar fields only in server-side code: an admin endpoint behind its own authorization check, sign-up, an invitation.
  6. Add MockMvc regression tests that send an ordinary update plus role, plus tenantId and plus case variants of both, then load the entity rather than only reading the response.

Vulnerable, fixed and wrong

The three implementations share one API and one test suite; only the code below changes. The tests load the one that IMPL names.

The vulnerable code

Vulnerable…/profile/impl/VulnerableProfileController.javaJava
@PatchMapping("/me")
User update(Principal principal, @RequestBody JsonNode changes) {
    User user = users.currentUser(principal);
    // Jackson calls the entity's setter for every property named in the body.
    json.readerForUpdating(user).readValue(changes);
    return user;
}

With IMPL=vulnerable, 2 of 6 tests fail (2 exploit tests).

The fix

Fixed…/profile/impl/FixedProfileController.javaJava
/**
 * The fields a user may change on their own profile. role and tenantId are not here:
 * only server-side code sets them. Jackson 3 ignores JSON properties a type does not
 * declare (FAIL_ON_UNKNOWN_PROPERTIES is off by default).
 */
record ProfileUpdate(String displayName, String bio) {}

@PatchMapping("/me")
User update(Principal principal, @RequestBody ProfileUpdate changes) {
    User user = users.currentUser(principal);
    if (changes.displayName() != null) {
        user.setDisplayName(changes.displayName());
    }
    if (changes.bio() != null) {
        user.setBio(changes.bio());
    }
    return user;
}

With IMPL=fixed, 6 of 6 tests pass.

A fix that looks safe but is not

Wrong fix…/profile/impl/WrongProfileController.javaJava
@PatchMapping("/me")
User update(Principal principal, @RequestBody ObjectNode changes) {
    User user = users.currentUser(principal);
    changes.remove("role"); // role is set by admins only
    json.readerForUpdating(user).readValue(changes);
    return user;
}

With IMPL=wrong, 1 of 6 tests fail (1 exploit test).

The regression test

The exploit tests send the attack and check that it fails; functional tests next to them keep ordinary requests working, so a fix that rejects everything fails too.

Test…/example/profile/ProfileApiTest.javaJava
// Exploit: a signed-in member sends extra fields in an ordinary profile update.

// The server may ignore the extra fields (200) or refuse the request (400). Either way the
// stored record keeps the role and tenant the server gave it. (User has no other fields to
// gain, unlike a map or a document.)
private void assertServerFieldsUnchanged(MockHttpServletResponse res) throws Exception {
    assertThat(res.getStatus()).as("status, body %s", body(res)).isIn(200, 400);
    User stored = users.find("alice");
    assertThat(stored.getRole()).as("role").isEqualTo("member");
    assertThat(stored.getTenantId()).as("tenantId").isEqualTo("red");
}

@Test
void exploitRoleInTheBodyDoesNotMakeAMemberAnAdmin() throws Exception {
    assertServerFieldsUnchanged(patchMe("alice", """
            {"displayName": "Alice Martin", "role": "admin"}"""));
}

@Test
void exploitTenantIdInTheBodyDoesNotMoveTheUserToAnotherTenant() throws Exception {
    assertServerFieldsUnchanged(patchMe("alice", """
            {"displayName": "Alice Martin", "tenantId": "blue"}"""));
}

@Test
void exploitCaseVariantsRoleAndTenantIdDoNotChangeTheRecordEither() throws Exception {
    assertServerFieldsUnchanged(patchMe("alice", """
            {"displayName": "Alice Martin", "Role": "admin", "TenantId": "blue"}"""));
}

Pitfalls

  • @JsonIgnoreProperties or @JsonProperty(access = READ_ONLY) on the entity is a blocklist: it protects the properties you annotated, and every property added later is writable. This card's wrong implementation removes role from the JSON and lets tenantId through, with the same effect.
  • @ModelAttribute and form binding go through DataBinder, which by default binds request parameters to every public setter, nested paths included. Spring's guidance is a record or dedicated input object, or setAllowedFields in an @InitBinder method; it calls setDisallowedFields fragile and says it is due to be deprecated in Spring Framework 7.1.
  • Jackson 3 turned FAIL_ON_UNKNOWN_PROPERTIES off by default (Jackson 2 had it on, and Spring Boot switched it off). That is why the record drops role quietly, and also why a probe for role succeeds with 200 and leaves no trace. Turn the feature on if you want such requests refused with 400.
  • Jackson matches property names case-sensitively unless spring.jackson.mapper.accept-case-insensitive-properties is on. With it on, a blocklist that compares exact names misses Role, as the ASP.NET Core card shows.
  • An input record is only as safe as its field list. Reusing the admin's UserUpdate record, which has role, for PATCH /me reopens the hole; use one record per action.
  • Hidden form fields and read-only inputs are not protection: the client can send any JSON it likes.

Verify the fix

  1. Run the tests in all three modes: IMPL=fixed must pass; IMPL=vulnerable and IMPL=wrong must fail (node ../../run-all.mjs mass-assignment/java-spring does this for you).
  2. Against your own API, sign in as an ordinary member, replay a normal profile update with role, tenantId and their case variants added, then load the entity from the database: role and tenantId must be unchanged.
  3. Do not rely on a scanner: no Semgrep or CodeQL rule covers mass assignment in Spring, Semgrep's OWASP Top 10 and Java packs reported nothing on the vulnerable code, and Find Security Bugs' ENTITY_MASS_ASSIGNMENT only looks for JPA, JDO and MongoDB entity types in controller signatures.

Scanner rules

What each rule reported when it was run on this card's code, or what its source shows it covers where it was not run. A rule that exists for the weakness does not always catch it.

Scanner rules for Fix mass assignment (over-posting) in Java and Spring Boot
ToolRuleWhat it found here
Semgrepp/owasp-top-tenNo findings on src/main/java: the vulnerable, fixed and wrong controllers look the same to every rule in these packs.Checked: Semgrep 1.180.0 (CE)
Find Security Bugs (SpotBugs plugin)ENTITY_MASS_ASSIGNMENTMass assignmentNot expected to report this card. It reports a @RequestMapping, @GetMapping, @PostMapping, @PutMapping, @DeleteMapping or @PatchMapping method whose parameter type is annotated @Entity (javax or jakarta persistence), JDO PersistenceCapable or Spring Data MongoDB @Document. This card's User is a plain class, and the vulnerable controller binds a JsonNode and copies it onto the entity with readerForUpdating, which a signature check cannot see. Binding @RequestBody to a JPA entity would be reported.Checked: Detector source read at find-sec-bugs master 90447f7 (SpringEntityLeakDetector.java); not executed

No Semgrep or CodeQL rule covers mass assignment in Spring: semgrep-rules develop at 9bbf021 has no Java or Kotlin rule tagged CWE-915, and github/codeql main at b5b8165 has no Java query tagged CWE-915. The regression tests are the control.

Libraries and versions

Libraries and versions
ComponentVersion
LanguageJava 21
RuntimeOpenJDK 21.0.12.1 (Homebrew), Maven 3.9.16
FrameworkSpring Boot 4.1.1 (Spring Framework 7.0.9, Spring MVC, Jackson 3)
org.springframework.boot:spring-boot-starter-parent4.1.1
org.springframework.boot:spring-boot-starter-webmvc4.1.1 (Spring Framework 7.0.9)
tools.jackson:jackson-bom (overrides Boot's 3.1.5)3.1.7 (jackson-databind 3.1.7)
org.apache.tomcat.embed:tomcat-embed-core (overrides Boot's 11.0.24)11.0.26
org.springframework.boot:spring-boot-starter-webmvc-test4.1.1
org.junit.jupiter:junit-jupiter (managed by Spring Boot)6.0.3

Run the tests

Download the project, install its dependencies once (this step needs the network), then run the tests against each implementation from the mass-assignment/java-springfolder. Maven runs with JDK 21 (set JAVA_HOME).

Shell
mvn -B -q test
Shell
# Expected: every test passes
IMPL=fixed mvn -B -o -q test

# Expected: an exploit test fails
IMPL=vulnerable mvn -B -o -q test

# Expected: a test fails
IMPL=wrong mvn -B -o -q test

Each run writes a JUnit report to target/surefire-reports/TEST-*.xml. From the top folder of the download, node run-all.mjs mass-assignment/java-spring runs all three modes and checks the expected results.

Download the project ZIP, 20 KB

Verified

Machine
macOS 27.0 (Darwin 27.0.0, arm64)
Toolchain
  • java OpenJDK 21.0.12.1 (Homebrew)
  • maven 3.9.16
  • springBoot 4.1.1
  • springFramework 7.0.9
  • jackson 3.1.7
  • tomcat 11.0.26
  • junitJupiter 6.0.3
Matrix
fixed: 6 of 6 tests pass; vulnerable: 2 of 6 tests fail (2 exploit tests); wrong: 1 of 6 tests fail (1 exploit test)
Test results
fixed 6/6 pass; vulnerable 2/6 fail (the role and tenantId exploit tests); wrong 1/6 fail (the tenantId exploit test). The case-variant test passes against all three because Jackson matches property names case-sensitively and ignores Role and TenantId; offline in a no-network sandbox, after installs with warm and with empty package caches
Dependency freshness
Spring Boot 4.1.1 was published on 2026-08-20. Boot 4.1.1 manages Tomcat 11.0.24 and Jackson 3.1.5, which have advisories fixed in Tomcat 11.0.26 and Jackson 3.1.7, so the pom overrides tomcat.version to 11.0.26 (2026-09-09) and jackson-bom.version to 3.1.7 (jackson-databind 3.1.7 reached Maven Central on 2026-09-22), as the other Spring cards do. OSV listed no advisories for the 72 resolved artifacts on 2026-10-10. Maven has no lockfile; the Boot parent and the two overrides fix every version.

Versions and results hold for the toolchain above. The card's tests are the check: run them with your own versions before relying on the fix.

References

  1. OWASP Mass Assignment Cheat Sheet
  2. OWASP API Security Top 10 2023: API3 Broken Object Property Level Authorization
  3. CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
  4. Spring Framework: Data Binding, Model Design
  5. Spring Boot: Customize the Jackson JsonMapper (spring.jackson.deserialization.*, spring.jackson.mapper.*)
  6. Jackson 3.1.7 ObjectMapper Javadoc (readerForUpdating, updateValue)
  7. Find Security Bugs: Mass assignment (ENTITY_MASS_ASSIGNMENT)

Find your next answer

Type to search. Search runs in your browser.

Searches run in your browser, but the host can infer them from the index files they load. The address shows your search so you can bookmark or share it.