Skip to content

Commit f02566a

Browse files
authored
Merge pull request #2002 from rabbitmq/mergify/bp/v5.x/pr-2000
Harden RPC support classes (backport #2000)
2 parents 38ef88e + c6089b0 commit f02566a

3 files changed

Lines changed: 130 additions & 27 deletions

File tree

src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcClient.java

Lines changed: 113 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@
2727
import java.lang.reflect.InvocationHandler;
2828
import java.lang.reflect.Method;
2929
import java.lang.reflect.Proxy;
30+
import java.util.Collections;
3031
import java.util.HashMap;
32+
import java.util.HashSet;
33+
import java.util.List;
3134
import java.util.Map;
35+
import java.util.Set;
3236
import java.util.concurrent.TimeoutException;
3337

3438
/**
@@ -54,6 +58,11 @@
5458
* <p>
5559
* {@link JsonRpcClient} delegates JSON parsing and generating to
5660
* a {@link JsonRpcMapper}.
61+
* <p>
62+
* By default only a safe set of return types is accepted from the remote
63+
* service description (primitives, {@link String}, {@link Map}, {@link List},
64+
* and common boxed types). Services that return custom types must pass an
65+
* explicit allowlist via the constructors that accept {@code allowedReturnTypes}.
5766
*
5867
* @see #call(String, Object[])
5968
* @see JsonRpcMapper
@@ -62,54 +71,113 @@
6271
public class JsonRpcClient extends RpcClient implements InvocationHandler {
6372

6473
private static final Logger LOGGER = LoggerFactory.getLogger(JsonRpcClient.class);
74+
75+
// Types that are safe to deserialize into without an explicit allowlist.
76+
private static final Set<String> SAFE_RETURN_TYPES;
77+
78+
static {
79+
Set<String> types = new HashSet<>();
80+
types.add("void");
81+
types.add("boolean");
82+
types.add("int");
83+
types.add("long");
84+
types.add("double");
85+
types.add("float");
86+
types.add("short");
87+
types.add("byte");
88+
types.add("char");
89+
types.add(String.class.getName());
90+
types.add(Boolean.class.getName());
91+
types.add(Integer.class.getName());
92+
types.add(Long.class.getName());
93+
types.add(Double.class.getName());
94+
types.add(Float.class.getName());
95+
types.add(Short.class.getName());
96+
types.add(Byte.class.getName());
97+
types.add(Number.class.getName());
98+
types.add(Map.class.getName());
99+
types.add(List.class.getName());
100+
types.add(Object.class.getName());
101+
SAFE_RETURN_TYPES = Collections.unmodifiableSet(types);
102+
}
103+
65104
private final JsonRpcMapper mapper;
105+
private final Set<String> allowedReturnTypes;
66106
/**
67107
* Holds the JSON-RPC service description for this client.
68108
*/
69109
private ServiceDescription serviceDescription;
70110

71111
/**
72-
* Construct a new {@link JsonRpcClient}, passing the {@link RpcClientParams} through {@link RpcClient}'s constructor.
112+
* Construct a new {@link JsonRpcClient} that only accepts return types from the built-in
113+
* safe set (primitives, {@link String}, {@link Map}, {@link List}, common boxed types).
73114
* <p>
74-
* The service description record is
75-
* retrieved from the server during construction.
115+
* Services that declare custom return types will cause construction to fail with an
116+
* {@link IllegalStateException}. Use
117+
* {@link #JsonRpcClient(RpcClientParams, JsonRpcMapper, Set)} to allow additional types.
76118
*
77-
* @param rpcClientParams
78-
* @param mapper
79-
* @throws IOException
80-
* @throws JsonRpcException
81-
* @throws TimeoutException
119+
* @throws IllegalStateException if the service description contains a return type not in
120+
* the allowlist
82121
*/
83122
public JsonRpcClient(RpcClientParams rpcClientParams, JsonRpcMapper mapper)
84123
throws IOException, JsonRpcException, TimeoutException {
124+
this(rpcClientParams, mapper, Collections.emptySet());
125+
}
126+
127+
/**
128+
* Construct a new {@link JsonRpcClient} with an explicit allowlist of permitted return types.
129+
* <p>
130+
* The provided types are merged with the built-in safe set, so common types ({@link String},
131+
* {@link Map}, primitives, etc.) do not need to be listed explicitly.
132+
*
133+
* @param allowedReturnTypes additional return types to accept from the remote service
134+
* description, on top of the built-in safe set
135+
* @throws IllegalStateException if the service description contains a return type not in
136+
* the allowlist
137+
*/
138+
public JsonRpcClient(RpcClientParams rpcClientParams, JsonRpcMapper mapper, Set<Class<?>> allowedReturnTypes)
139+
throws IOException, JsonRpcException, TimeoutException {
85140
super(rpcClientParams);
86141
this.mapper = mapper;
142+
this.allowedReturnTypes = buildAllowedReturnTypes(allowedReturnTypes);
87143
retrieveServiceDescription();
144+
validateReturnTypes();
88145
}
89146

90147
/**
91-
* Construct a new JsonRpcClient, passing the parameters through
92-
* to RpcClient's constructor. The service description record is
93-
* retrieved from the server during construction.
148+
* Construct a new JsonRpcClient using the safe default return-type allowlist.
94149
*
95150
* @throws TimeoutException if a response is not received within the timeout specified, if any
151+
* @throws IllegalStateException if the service description contains a return type not in
152+
* the allowlist
96153
*/
97154
public JsonRpcClient(Channel channel, String exchange, String routingKey, int timeout, JsonRpcMapper mapper)
98155
throws IOException, JsonRpcException, TimeoutException {
99-
super(new RpcClientParams()
100-
.channel(channel)
101-
.exchange(exchange)
102-
.routingKey(routingKey)
103-
.timeout(timeout)
104-
);
105-
this.mapper = mapper;
106-
retrieveServiceDescription();
156+
this(
157+
new RpcClientParams().channel(channel).exchange(exchange).routingKey(routingKey).timeout(timeout),
158+
mapper);
107159
}
108160

109161
/**
110-
* Construct a new JsonRpcClient, passing the parameters through
111-
* to RpcClient's constructor. The service description record is
112-
* retrieved from the server during construction.
162+
* Construct a new JsonRpcClient with an explicit allowlist of permitted return types.
163+
*
164+
* @param allowedReturnTypes additional return types to accept from the remote service
165+
* description, on top of the built-in safe set
166+
* @throws TimeoutException if a response is not received within the timeout specified, if any
167+
* @throws IllegalStateException if the service description contains a return type not in
168+
* the allowlist
169+
*/
170+
public JsonRpcClient(Channel channel, String exchange, String routingKey, int timeout, JsonRpcMapper mapper,
171+
Set<Class<?>> allowedReturnTypes)
172+
throws IOException, JsonRpcException, TimeoutException {
173+
this(
174+
new RpcClientParams().channel(channel).exchange(exchange).routingKey(routingKey).timeout(timeout),
175+
mapper,
176+
allowedReturnTypes);
177+
}
178+
179+
/**
180+
* Construct a new JsonRpcClient using the safe default return-type allowlist.
113181
*
114182
* @throws TimeoutException if a response is not received within the timeout specified, if any
115183
*/
@@ -123,6 +191,29 @@ public JsonRpcClient(Channel channel, String exchange, String routingKey)
123191
this(channel, exchange, routingKey, RpcClient.NO_TIMEOUT);
124192
}
125193

194+
private static Set<String> buildAllowedReturnTypes(Set<Class<?>> extraTypes) {
195+
if (extraTypes == null || extraTypes.isEmpty()) {
196+
return SAFE_RETURN_TYPES;
197+
}
198+
Set<String> types = new HashSet<>(SAFE_RETURN_TYPES);
199+
for (Class<?> c : extraTypes) {
200+
types.add(c.getName());
201+
}
202+
return Collections.unmodifiableSet(types);
203+
}
204+
205+
private void validateReturnTypes() {
206+
for (ProcedureDescription proc : serviceDescription.getProcs()) {
207+
String javaReturnType = proc.getJavaReturnType();
208+
if (javaReturnType != null && !allowedReturnTypes.contains(javaReturnType)) {
209+
throw new IllegalStateException(
210+
"Return type not in allowlist: '"
211+
+ javaReturnType
212+
+ "'. Pass it via the allowedReturnTypes constructor parameter.");
213+
}
214+
}
215+
}
216+
126217
/**
127218
* Private API - used by {@link #call(String[])} to ad-hoc convert
128219
* strings into the required data types for a call.

src/main/java/com/rabbitmq/tools/jsonrpc/ProcedureDescription.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private Class<?> computeReturnTypeAsJavaClass() {
119119
} else if ("void".equals(javaReturnType)) {
120120
return Void.TYPE;
121121
} else {
122-
return Class.forName(javaReturnType);
122+
return Class.forName(javaReturnType, false, Thread.currentThread().getContextClassLoader());
123123
}
124124
} catch (ClassNotFoundException e) {
125125
throw new IllegalStateException("Unable to load class: " + javaReturnType, e);

src/test/java/com/rabbitmq/client/AbstractJsonRpcTest.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@
2222
import org.junit.jupiter.api.AfterEach;
2323
import org.junit.jupiter.api.BeforeEach;
2424

25+
import java.util.Arrays;
2526
import java.util.Date;
27+
import java.util.HashSet;
28+
import java.util.Set;
2629

2730
public abstract class AbstractJsonRpcTest {
2831

32+
private static final Set<Class<?>> ALLOWED_RETURN_TYPES = new HashSet<>(Arrays.asList(
33+
Date.class, Pojo.class
34+
));
35+
2936
Connection clientConnection, serverConnection;
3037
Channel clientChannel, serverChannel;
3138
String queue = "json.rpc.queue";
@@ -51,10 +58,15 @@ public void init() throws Exception {
5158
// safe to ignore when loops ends/server is canceled
5259
}
5360
}).start();
54-
client = new JsonRpcClient(
55-
new RpcClientParams().channel(clientChannel).exchange("").routingKey(queue).timeout(1000),
56-
createMapper()
57-
);
61+
client =
62+
new JsonRpcClient(
63+
new RpcClientParams()
64+
.channel(clientChannel)
65+
.exchange("")
66+
.routingKey(queue)
67+
.timeout(1000),
68+
createMapper(),
69+
ALLOWED_RETURN_TYPES);
5870
service = client.createProxy(RpcService.class);
5971
}
6072

0 commit comments

Comments
 (0)