Skip to content

Commit 9b55ac8

Browse files
authored
Merge pull request #788 from marklogic/feature/polaris
MLE-24375 Fixing URI construction
2 parents 1b0bcee + 00d4720 commit 9b55ac8

File tree

5 files changed

+100
-56
lines changed

5 files changed

+100
-56
lines changed

ml-app-deployer/src/main/java/com/marklogic/mgmt/resource/AbstractResourceManager.java

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@ public String getResourcesPath() {
3636
}
3737

3838
public String getResourcePath(String resourceNameOrId, String... resourceUrlParams) {
39-
resourceNameOrId = encodeResourceId(resourceNameOrId);
39+
resourceNameOrId = encodeResourceIdBeforeAddingToPath(resourceNameOrId);
4040
return appendParamsAndValuesToPath(format("%s/%s", getResourcesPath(), resourceNameOrId), resourceUrlParams);
4141
}
4242

4343
public String getPropertiesPath(String resourceNameOrId, String... resourceUrlParams) {
44-
return appendParamsAndValuesToPath(format("%s/properties", getResourcePath(resourceNameOrId)),
45-
resourceUrlParams);
44+
String resourcePath = getResourcePath(resourceNameOrId);
45+
String propertiesPath = format("%s/properties", resourcePath);
46+
return appendParamsAndValuesToPath(propertiesPath, resourceUrlParams);
4647
}
4748

4849
/**
@@ -62,27 +63,27 @@ public ResourcesFragment getAsXml() {
6263
}
6364

6465
public Fragment getAsXml(String resourceNameOrId, String... resourceUrlParams) {
65-
String path = appendParamsAndValuesToPath(getResourcePath(resourceNameOrId, resourceUrlParams));
66+
String path = getResourcePath(resourceNameOrId, resourceUrlParams);
6667
return useSecurityUser() ? manageClient.getXmlAsSecurityUser(path) : manageClient.getXml(path);
6768
}
6869

6970
public Fragment getPropertiesAsXml(String resourceNameOrId, String... resourceUrlParams) {
70-
String path = appendParamsAndValuesToPath(getPropertiesPath(resourceNameOrId, resourceUrlParams));
71+
String path = getPropertiesPath(resourceNameOrId, resourceUrlParams);
7172
return useSecurityUser() ? manageClient.getXmlAsSecurityUser(path) : manageClient.getXml(path);
7273
}
7374

7475
public String getPropertiesAsXmlString(String resourceNameOrId, String... resourceUrlParams) {
75-
String path = appendParamsAndValuesToPath(getPropertiesPath(resourceNameOrId, resourceUrlParams));
76+
String path = getPropertiesPath(resourceNameOrId, resourceUrlParams);
7677
return useSecurityUser() ? manageClient.getXmlStringAsSecurityUser(path) : manageClient.getXmlString(path);
7778
}
7879

7980
public String getAsJson(String resourceNameOrId, String... resourceUrlParams) {
80-
String path = appendParamsAndValuesToPath(getPropertiesPath(resourceNameOrId, resourceUrlParams));
81+
String path = getPropertiesPath(resourceNameOrId, resourceUrlParams);
8182
return useSecurityUser() ? manageClient.getJsonAsSecurityUser(path) : manageClient.getJson(path);
8283
}
8384

8485
public String getPropertiesAsJson(String resourceNameOrId, String... resourceUrlParams) {
85-
String path = appendParamsAndValuesToPath(getPropertiesPath(resourceNameOrId, resourceUrlParams));
86+
String path = getPropertiesPath(resourceNameOrId, resourceUrlParams);
8687
return useSecurityUser() ? manageClient.getJsonAsSecurityUser(path) : manageClient.getJson(path);
8788
}
8889

@@ -123,18 +124,17 @@ protected SaveReceipt createNewResource(String payload, String resourceId) {
123124
}
124125

125126
/**
126-
* Mimetypes are likely to have a "+" in them, which the Management REST API won't support in a path - it needs to
127-
* be encoded. Other resources could have a "+" in their ID value as well. However, doing a full encoding doesn't
128-
* seem to be a great idea, as that will e.g. encode a forward slash in a mimetype as well, which will result in a
129-
* 404.
127+
* The resource ID needs to be encoded for one known reason, which was first discovered in May 2016 - mimetypes
128+
* can have "+" in them, but the server erroneously does not support a "+" in the path part of a URI. So the "+"
129+
* needs to be encoded. For example, "http://localhost:8002/manage/v2/mimetypes/text/foo+bar" doesn't work,
130+
* but "http://localhost:8002/manage/v2/mimetypes/text/foo%2Bbar" does work.
131+
*
132+
* See MLE-24380 for the internal server bug.
130133
*
131134
* @param idValue
132135
* @return
133136
*/
134-
protected String encodeResourceId(String idValue) {
135-
// Polaris effectively complains that this is allowing server-side request forgery. However, we cannot perform
136-
// a full URL encoding here as "/" is allowed in some resource names, such as MIME types. Polaris also is
137-
// seemingly only seeing the path and not the fact that the ManageClient will always build an http or https URL.
137+
protected final String encodeResourceIdBeforeAddingToPath(String idValue) {
138138
return idValue != null ? idValue.replace("+", "%2B") : idValue;
139139
}
140140

ml-app-deployer/src/main/java/com/marklogic/mgmt/resource/security/ProtectedCollectionsManager.java

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
*/
44
package com.marklogic.mgmt.resource.security;
55

6-
import com.marklogic.mgmt.resource.AbstractResourceManager;
76
import com.marklogic.mgmt.ManageClient;
8-
9-
import java.io.UnsupportedEncodingException;
10-
import java.net.URLEncoder;
7+
import com.marklogic.mgmt.resource.AbstractResourceManager;
118

129
public class ProtectedCollectionsManager extends AbstractResourceManager {
1310

@@ -32,28 +29,11 @@ protected String getIdFieldName() {
3229

3330
@Override
3431
public String getPropertiesPath(String resourceNameOrId, String... resourceUrlParams) {
35-
return getResourcesPath() + "/properties?collection=" + encodeCollectionName(resourceNameOrId);
32+
return getResourcesPath() + "/properties?collection=" + resourceNameOrId;
3633
}
3734

3835
@Override
3936
public String getResourcePath(String resourceNameOrId, String... resourceUrlParams) {
40-
return getResourcesPath() + "?collection=" + encodeCollectionName(resourceNameOrId);
37+
return getResourcesPath() + "?collection=" + resourceNameOrId;
4138
}
42-
43-
/**
44-
* For most Manage API resources, MarkLogic prohibits characters that require URL encoding. Collection names are
45-
* an exception though and thus require URL encoding so that their names can be used in a querystring.
46-
*
47-
* @param collectionName
48-
* @return
49-
*/
50-
private String encodeCollectionName(String collectionName) {
51-
try {
52-
return URLEncoder.encode(collectionName, "utf-8");
53-
} catch (UnsupportedEncodingException e) {
54-
logger.warn(format("Unable to encode collection: %s; will include un-encoded collection name in " +
55-
"querystring; cause: %s", collectionName, e.getMessage()));
56-
return collectionName;
57-
}
58-
}
5939
}

ml-app-deployer/src/main/java/com/marklogic/mgmt/resource/temporal/TemporalCollectionManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public String getResourcesPath() {
2222

2323
@Override
2424
public String getResourcePath(String resourceNameOrId, String... resourceUrlParams) {
25-
resourceNameOrId = encodeResourceId(resourceNameOrId);
25+
resourceNameOrId = encodeResourceIdBeforeAddingToPath(resourceNameOrId);
2626
return appendParamsAndValuesToPath(format("%s?collection=%s", getResourcesPath(), resourceNameOrId), resourceUrlParams);
2727
}
2828

ml-app-deployer/src/main/java/com/marklogic/rest/util/RestConfig.java

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
import com.marklogic.client.ext.ssl.SslUtil;
1212
import okhttp3.OkHttpClient;
1313
import org.springframework.util.StringUtils;
14+
import org.springframework.web.util.UriComponentsBuilder;
1415

1516
import javax.net.ssl.SSLContext;
1617
import java.net.URI;
17-
import java.net.URISyntaxException;
1818

1919
public class RestConfig {
2020

@@ -158,30 +158,94 @@ public String toString() {
158158
}
159159

160160
/**
161-
* Using the java.net.URI constructor that takes a string. Using any other constructor runs into encoding problems,
162-
* e.g. when a mimetype has a plus in it, that plus needs to be encoded, but doing as %2B will result in the % being
163-
* double encoded. Unfortunately, it seems some encoding is still needed - e.g. for a pipeline like "Flexible Replication"
164-
* with a space in its name, the space must be encoded properly as a "+".
161+
* Builds a URI using Spring's UriComponentsBuilder to prevent URL manipulation attacks.
162+
* This replaces the previous vulnerable implementation that allowed user-controllable URL construction.
163+
* Handles query parameters that may be included in the path for backwards compatibility.
165164
*
166165
* @param path
167166
* @return
168167
*/
169168
public URI buildUri(String path) {
170-
String basePathToAppend = "";
169+
try {
170+
UriComponentsBuilder builder = UriComponentsBuilder.newInstance()
171+
.scheme(getScheme())
172+
.host(getHost())
173+
.port(getPort());
174+
175+
final String fullPath = determineFullPath(path);
176+
177+
int queryIndex = fullPath.indexOf('?');
178+
if (queryIndex != -1) {
179+
String pathPart = fullPath.substring(0, queryIndex);
180+
builder.path(pathPart);
181+
String queryPart = fullPath.substring(queryIndex + 1);
182+
applyQueryParams(builder, queryPart);
183+
} else {
184+
builder.path(fullPath);
185+
}
186+
187+
URI uri = builder.build().toUri();
188+
uri = fixDoubleEncodingOfPlusSign(uri);
189+
return uri;
190+
} catch (Exception ex) {
191+
throw new RuntimeException("Unable to build URI for path: " + path + "; cause: " + ex.getMessage(), ex);
192+
}
193+
}
194+
195+
private String determineFullPath(String path) {
196+
String fullPath = path;
171197
if (basePath != null) {
172-
if (!basePath.startsWith("/")) {
173-
basePathToAppend = "/";
198+
String normalizedBasePath = basePath.startsWith("/") ? basePath : "/" + basePath;
199+
if (path.startsWith("/") && normalizedBasePath.endsWith("/")) {
200+
normalizedBasePath = normalizedBasePath.substring(0, normalizedBasePath.length() - 1);
174201
}
175-
basePathToAppend += basePath;
176-
if (path.startsWith("/") && basePathToAppend.endsWith("/")) {
177-
basePathToAppend = basePathToAppend.substring(0, basePathToAppend.length() - 1);
202+
fullPath = normalizedBasePath + path;
203+
}
204+
return fullPath;
205+
}
206+
207+
private UriComponentsBuilder applyQueryParams(UriComponentsBuilder builder, String queryPart) {
208+
// Parse and add query parameters
209+
if (!queryPart.isEmpty()) {
210+
String[] params = queryPart.split("&");
211+
for (String param : params) {
212+
int equalIndex = param.indexOf('=');
213+
if (equalIndex != -1) {
214+
String key = param.substring(0, equalIndex);
215+
String value = param.substring(equalIndex + 1);
216+
builder.queryParam(key, value);
217+
} else {
218+
// Handle parameters without values
219+
builder.queryParam(param, "");
220+
}
178221
}
179222
}
180-
try {
181-
return new URI(String.format("%s://%s:%d%s%s", getScheme(), getHost(), getPort(), basePathToAppend, path.replace(" ", "+")));
182-
} catch (URISyntaxException ex) {
183-
throw new RuntimeException("Unable to build URI for path: " + path + "; cause: " + ex.getMessage(), ex);
223+
return builder;
224+
}
225+
226+
private URI fixDoubleEncodingOfPlusSign(final URI uri) {
227+
// Fix double-encoding issue: MarkLogic requires "+" to be encoded as "%2B" in paths (at least for mimetypes),
228+
// but UriComponentsBuilder double-encodes it to "%252B".
229+
String uriString = uri.toString();
230+
if (uriString.contains("%252B")) {
231+
try {
232+
// Split the URI to handle path and query separately
233+
String[] parts = uriString.split("\\?", 2);
234+
String pathPart = parts[0];
235+
String queryPart = parts.length > 1 ? parts[1] : null;
236+
237+
// Only fix double-encoding in the path part
238+
if (pathPart.contains("%252B")) {
239+
pathPart = pathPart.replace("%252B", "%2B");
240+
String fixedUriString = queryPart != null ? pathPart + "?" + queryPart : pathPart;
241+
return new URI(fixedUriString);
242+
}
243+
} catch (Exception e) {
244+
// If URI reconstruction fails, return the original URI
245+
// This preserves existing behavior in case of unexpected issues
246+
}
184247
}
248+
return uri;
185249
}
186250

187251
public String getBaseUrl() {

ml-app-deployer/src/test/java/com/marklogic/appdeployer/command/mimetypes/ManageMimetypesTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import com.marklogic.mgmt.resource.ResourceManager;
99
import com.marklogic.mgmt.resource.mimetypes.MimetypeManager;
1010

11-
public class ManageMimetypesTest extends AbstractManageResourceTest {
11+
class ManageMimetypesTest extends AbstractManageResourceTest {
1212

1313
@Override
1414
protected ResourceManager newResourceManager() {

0 commit comments

Comments
 (0)