Skip to content

Commit d627a8a

Browse files
authored
Fix #6060 -- @JsonView skipped for Setter/Field properties with @JsonUnwrapped -- (for 2.18 and above) (#6056)
1 parent bc1613c commit d627a8a

3 files changed

Lines changed: 124 additions & 0 deletions

File tree

‎release-notes/VERSION-2.x‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Project: jackson-databind
99
#5962: Case-insensitive deserialization may use wrong `@JsonIgnoreProperties`
1010
[CVE-2026-54515]
1111
(fixed by Omkhar A)
12+
#6060: `@JsonView` by-passed for `@JsonUnwrapped` Field/Setter properties
1213

1314
2.18.8 (28-May-2026)
1415

‎src/main/java/com/fasterxml/jackson/databind/deser/impl/UnwrappedPropertyHandler.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,14 @@ public Object processUnwrapped(JsonParser originalParser, DeserializationContext
5656
Object bean, TokenBuffer buffered)
5757
throws IOException
5858
{
59+
// [databind#6060]: honor active @JsonView -- skip Field/Setter properties not
60+
// visible in the active view rather than populating them from buffered input.
61+
final Class<?> activeView = ctxt.getActiveView();
5962
for (int i = 0, len = _properties.size(); i < len; ++i) {
6063
SettableBeanProperty prop = _properties.get(i);
64+
if ((activeView != null) && !prop.visibleInView(activeView)) {
65+
continue;
66+
}
6167
JsonParser p = buffered.asParser(originalParser.streamReadConstraints());
6268
p.nextToken();
6369
prop.deserializeAndSet(p, ctxt, bean);
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.fasterxml.jackson.databind.views;
2+
3+
import com.fasterxml.jackson.annotation.*;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertNotNull;
10+
import static org.junit.jupiter.api.Assertions.assertNull;
11+
12+
// [databind#6060]: `@JsonView` by-passed for `@JsonUnwrapped` Field/Setter properties
13+
public class UnwrappedViewBypass6060Test extends DatabindTestUtil {
14+
public static class PublicView {}
15+
public static class AdminView extends PublicView {}
16+
17+
public static class AccountFlags { // admin-only block
18+
public String role;
19+
public boolean approved;
20+
public long creditBalance;
21+
}
22+
public static class Registration { // bound at the public registration endpoint
23+
@JsonView(PublicView.class) public String email;
24+
@JsonView(PublicView.class) public String password;
25+
@JsonView(AdminView.class) @JsonUnwrapped public AccountFlags flags; // admin-only, unwrapped
26+
}
27+
public static class Control { // identical block, NOT unwrapped
28+
@JsonView(PublicView.class) public String email;
29+
@JsonView(AdminView.class) public AccountFlags flags;
30+
}
31+
32+
public static class RegistrationWithSetters { // same as Registration, but via setters
33+
String email;
34+
String password;
35+
AccountFlags flags;
36+
37+
@JsonView(PublicView.class)
38+
public void setEmail(String email) { this.email = email; }
39+
40+
@JsonView(PublicView.class)
41+
public void setPassword(String password) { this.password = password; }
42+
43+
@JsonView(AdminView.class) @JsonUnwrapped
44+
public void setFlags(AccountFlags flags) { this.flags = flags; }
45+
}
46+
47+
@Test
48+
void testUnwrappedBypass() throws Exception {
49+
ObjectMapper mapper = sharedMapper();
50+
51+
String jsonC = a2q("{'email':'e','flags':{'role':'ADMIN','approved':true,'creditBalance':1000000}}");
52+
Control c = mapper.readerWithView(PublicView.class)
53+
.forType(Control.class)
54+
.readValue(jsonC);
55+
assertEquals("e", c.email);
56+
assertNull(c.flags, "expected control flag to be null in PublicView read");
57+
58+
String jsonR = a2q("{'email':'mallory@evil.test','password':'pw',"
59+
+ "'role':'ADMIN','approved':true,'creditBalance':1000000}");
60+
Registration r = mapper.readerWithView(PublicView.class)
61+
.forType(Registration.class)
62+
.readValue(jsonR);
63+
assertEquals("mallory@evil.test", r.email);
64+
assertEquals("pw", r.password);
65+
// JsonUnwrapped flag in Registration class may affect this
66+
assertNull(r.flags, "expected registration flag to be null in PublicView read");
67+
}
68+
69+
// Negative control: AdminView read MUST populate the unwrapped admin-only block,
70+
// so the PublicView assertion above is not passing vacuously.
71+
@Test
72+
void testUnwrappedVisibleInAdminView() throws Exception {
73+
ObjectMapper mapper = sharedMapper();
74+
75+
String jsonR = a2q("{'email':'admin@corp.test','password':'pw',"
76+
+ "'role':'ADMIN','approved':true,'creditBalance':1000000}");
77+
Registration r = mapper.readerWithView(AdminView.class)
78+
.forType(Registration.class)
79+
.readValue(jsonR);
80+
assertEquals("admin@corp.test", r.email);
81+
assertNotNull(r.flags, "expected registration flag to be populated in AdminView read");
82+
assertEquals("ADMIN", r.flags.role);
83+
assertEquals(1000000, r.flags.creditBalance);
84+
}
85+
86+
@Test
87+
void testUnwrappedBypassWithSetters() throws Exception {
88+
ObjectMapper mapper = sharedMapper();
89+
90+
String jsonR = a2q("{'email':'mallory@evil.test','password':'pw',"
91+
+ "'role':'ADMIN','approved':true,'creditBalance':1000000}");
92+
RegistrationWithSetters r = mapper.readerWithView(PublicView.class)
93+
.forType(RegistrationWithSetters.class)
94+
.readValue(jsonR);
95+
assertEquals("mallory@evil.test", r.email);
96+
assertEquals("pw", r.password);
97+
// JsonUnwrapped flag in RegistrationWithSetters class may affect this
98+
assertNull(r.flags, "expected registration flag to be null in PublicView read");
99+
}
100+
101+
// Negative control for the setter variant: AdminView read MUST populate flags.
102+
@Test
103+
void testUnwrappedVisibleInAdminViewWithSetters() throws Exception {
104+
ObjectMapper mapper = sharedMapper();
105+
106+
String jsonR = a2q("{'email':'admin@corp.test','password':'pw',"
107+
+ "'role':'ADMIN','approved':true,'creditBalance':1000000}");
108+
RegistrationWithSetters r = mapper.readerWithView(AdminView.class)
109+
.forType(RegistrationWithSetters.class)
110+
.readValue(jsonR);
111+
assertEquals("admin@corp.test", r.email);
112+
assertNotNull(r.flags, "expected registration flag to be populated in AdminView read");
113+
assertEquals("ADMIN", r.flags.role);
114+
assertEquals(1000000, r.flags.creditBalance);
115+
}
116+
117+
}

0 commit comments

Comments
 (0)