Skip to content

Commit 7eddcae

Browse files
committed
Update RESTClientUtility to fix a security issue
1 parent 930309a commit 7eddcae

1 file changed

Lines changed: 61 additions & 12 deletions

File tree

roda-common/roda-common-utils/src/main/java/org/roda/core/util/RESTClientUtility.java

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
import java.io.IOException;
1313
import java.io.InputStream;
1414
import java.io.Serializable;
15+
import java.net.InetAddress;
16+
import java.net.URI;
17+
import java.net.URISyntaxException;
18+
import java.net.UnknownHostException;
1519
import java.nio.CharBuffer;
1620
import java.nio.charset.Charset;
1721
import java.nio.charset.StandardCharsets;
@@ -43,6 +47,9 @@
4347
*/
4448
public final class RESTClientUtility {
4549

50+
public static final String AUTHORIZATION = "Authorization";
51+
public static final String BEARER = "Bearer ";
52+
4653
/** Private empty constructor */
4754
private RESTClientUtility() {
4855
// do nothing
@@ -51,10 +58,10 @@ private RESTClientUtility() {
5158
public static <T extends Serializable> T sendPostRequestWithoutBodyHttp5(Class<T> elementClass, String url,
5259
String resource, AccessToken accessToken) throws GenericException {
5360
try (final CloseableHttpClient httpclient = HttpClients.createDefault()) {
54-
HttpPost httpPost = new HttpPost(url + resource);
55-
httpPost.addHeader("Authorization", "Bearer " + accessToken.getToken());
56-
httpPost.addHeader("content-type", "application/json");
57-
httpPost.addHeader("Accept", "application/json");
61+
HttpPost httpPost = new HttpPost(buildSafeUri(url, resource));
62+
httpPost.addHeader(AUTHORIZATION, BEARER + accessToken.getToken());
63+
httpPost.addHeader("content-type", RodaConstants.MEDIA_TYPE_APPLICATION_JSON);
64+
httpPost.addHeader("Accept", RodaConstants.MEDIA_TYPE_APPLICATION_JSON);
5865

5966
Result result = httpclient.execute(httpPost, response -> new Result(response.getCode(),
6067
EntityUtils.toString(response.getEntity(), Charset.defaultCharset())));
@@ -76,10 +83,10 @@ public static <T extends Serializable> T sendPostRequestWithoutBodyHttp5(Class<T
7683
public static <T extends Serializable> T sendPostRequestHttpClient5(Object element, Class<T> elementClass, String url,
7784
String resource, AccessToken accessToken) throws GenericException {
7885
try (final CloseableHttpClient httpclient = HttpClients.createDefault()) {
79-
HttpPost httpPost = new HttpPost(url + resource);
80-
httpPost.addHeader("Authorization", "Bearer " + accessToken.getToken());
81-
httpPost.addHeader("content-type", "application/json");
82-
httpPost.addHeader("Accept", "application/json");
86+
HttpPost httpPost = new HttpPost(buildSafeUri(url, resource));
87+
httpPost.addHeader(AUTHORIZATION, BEARER + accessToken.getToken());
88+
httpPost.addHeader("content-type", RodaConstants.MEDIA_TYPE_APPLICATION_JSON);
89+
httpPost.addHeader("Accept", RodaConstants.MEDIA_TYPE_APPLICATION_JSON);
8390

8491
httpPost.setEntity(new StringEntity(JsonUtils.getJsonFromObject(element)));
8592

@@ -103,8 +110,8 @@ public static <T extends Serializable> T sendPostRequestHttpClient5(Object eleme
103110
public static int sendPostRequestWithCompressedFileHttp5(String url, String resource, Path path,
104111
AccessToken accessToken) throws RODAException {
105112
try (final CloseableHttpClient httpclient = HttpClients.createDefault()) {
106-
HttpPost httpPost = new HttpPost(url + resource);
107-
httpPost.addHeader("Authorization", "Bearer " + accessToken.getToken());
113+
HttpPost httpPost = new HttpPost(buildSafeUri(url, resource));
114+
httpPost.addHeader(AUTHORIZATION, BEARER + accessToken.getToken());
108115

109116
InputStream inputStream = Files.newInputStream(path.toFile().toPath());
110117
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
@@ -124,14 +131,14 @@ public static int sendPostRequestWithCompressedFileHttp5(String url, String reso
124131
public static int sendPostRequestWithFileHttp5(String url, String resource, String username, SecureString password,
125132
Path file) throws RODAException {
126133
try (final CloseableHttpClient httpclient = HttpClients.createDefault()) {
127-
HttpPost httpPost = new HttpPost(url + resource);
134+
HttpPost httpPost = new HttpPost(buildSafeUri(url, resource));
128135
try (
129136
SecureString basicAuth = new SecureString(
130137
ArrayUtils.addAll((username + ":").toCharArray(), password.getChars()));
131138
SecureString basicAuthToken = new SecureString(
132139
Base64.encode(StandardCharsets.UTF_8.encode(CharBuffer.wrap(basicAuth)).array()))) {
133140

134-
httpPost.setHeader("Authorization", "Basic " + basicAuthToken);
141+
httpPost.setHeader(AUTHORIZATION, "Basic " + basicAuthToken);
135142
File fileToUpload = new File(FilenameUtils.normalize(file.toString()));
136143
InputStream inputStream = new FileInputStream(fileToUpload);
137144
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
@@ -148,6 +155,48 @@ public static int sendPostRequestWithFileHttp5(String url, String resource, Stri
148155
}
149156
}
150157

158+
private static URI buildSafeUri(String baseUrl, String resource) throws GenericException {
159+
if (baseUrl == null) {
160+
throw new GenericException("Base URL must not be null");
161+
}
162+
try {
163+
URI base = new URI(baseUrl.trim());
164+
String scheme = base.getScheme();
165+
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
166+
throw new GenericException("Unsupported URL scheme for central instance: " + scheme);
167+
}
168+
if (base.getHost() == null) {
169+
throw new GenericException("Central instance URL must include a host");
170+
}
171+
if (base.getUserInfo() != null) {
172+
throw new GenericException("User info is not allowed in central instance URL");
173+
}
174+
175+
// Optional but safer: prevent requests to loopback or private addresses
176+
try {
177+
InetAddress addr = InetAddress.getByName(base.getHost());
178+
if (addr.isAnyLocalAddress() || addr.isLoopbackAddress() || addr.isSiteLocalAddress()) {
179+
throw new GenericException("Central instance URL points to a disallowed address");
180+
}
181+
} catch (UnknownHostException e) {
182+
throw new GenericException("Cannot resolve central instance host", e);
183+
}
184+
185+
String basePath = base.getPath() == null ? "" : base.getPath();
186+
String resPath = resource == null ? "" : resource;
187+
// Ensure exactly one slash between base path and resource
188+
if (!basePath.endsWith("/") && !resPath.startsWith("/")) {
189+
resPath = RodaConstants.API_SEP + resPath;
190+
} else if (basePath.endsWith("/") && resPath.startsWith("/")) {
191+
resPath = resPath.substring(1);
192+
}
193+
return new URI(base.getScheme(), base.getUserInfo(), base.getHost(), base.getPort(), basePath + resPath, null,
194+
null);
195+
} catch (URISyntaxException e) {
196+
throw new GenericException("Invalid central instance URL", e);
197+
}
198+
}
199+
151200
private record Result(int statusCode, String content) {
152201
}
153202
}

0 commit comments

Comments
 (0)