Skip to content

Commit 066db76

Browse files
authored
Merge pull request #18153 from owen-mc/java/resttemplate-getforobject
Java: add SSRF sink model for the third parameter of `RestTemplate.getForObject`
2 parents 538dee8 + 1420bce commit 066db76

File tree

4 files changed

+99
-7
lines changed

4 files changed

+99
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added a sink for "Server-side request forgery" (`java/ssrf`) for the third parameter to org.springframework.web.client.RestTemplate.getForObject, when we cannot statically determine that it does not affect the host in the URL.

java/ql/lib/semmle/code/java/frameworks/spring/SpringWebClient.qll

+74
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java
66
import SpringHttp
7+
private import semmle.code.java.security.RequestForgery
78

89
/** The class `org.springframework.web.client.RestTemplate`. */
910
class SpringRestTemplate extends Class {
@@ -27,3 +28,76 @@ class SpringWebClient extends Interface {
2728
this.hasQualifiedName("org.springframework.web.reactive.function.client", "WebClient")
2829
}
2930
}
31+
32+
/** The method `getForObject` on `org.springframework.web.reactive.function.client.RestTemplate`. */
33+
class SpringRestTemplateGetForObjectMethod extends Method {
34+
SpringRestTemplateGetForObjectMethod() {
35+
this.getDeclaringType() instanceof SpringRestTemplate and
36+
this.hasName("getForObject")
37+
}
38+
}
39+
40+
/** A call to the method `getForObject` on `org.springframework.web.reactive.function.client.RestTemplate`. */
41+
class SpringRestTemplateGetForObjectMethodCall extends MethodCall {
42+
SpringRestTemplateGetForObjectMethodCall() {
43+
this.getMethod() instanceof SpringRestTemplateGetForObjectMethod
44+
}
45+
46+
/** Gets the first argument, if it is a compile time constant. */
47+
CompileTimeConstantExpr getConstantUrl() { result = this.getArgument(0) }
48+
49+
/**
50+
* Holds if the first argument is a compile time constant and it has a
51+
* placeholder at offset `offset`, and there are `idx` placeholders that
52+
* appear before it.
53+
*/
54+
predicate urlHasPlaceholderAtOffset(int idx, int offset) {
55+
exists(
56+
this.getConstantUrl()
57+
.getStringValue()
58+
.replaceAll("\\{", " ")
59+
.replaceAll("\\}", " ")
60+
.regexpFind("\\{[^}]*\\}", idx, offset)
61+
)
62+
}
63+
}
64+
65+
private class SpringWebClientRestTemplateGetForObject extends RequestForgerySink {
66+
SpringWebClientRestTemplateGetForObject() {
67+
exists(SpringRestTemplateGetForObjectMethodCall mc, int i |
68+
// Note that the first argument is modeled as a request forgery sink
69+
// separately. This model is for arguments beyond the first two. There
70+
// are two relevant overloads, one with third parameter type `Object...`
71+
// and one with third parameter type `Map<String, ?>`. For the latter we
72+
// cannot deal with MapValue content easily but there is a default
73+
// implicit taint read at sinks that will catch it.
74+
i >= 0 and
75+
this.asExpr() = mc.getArgument(i + 2)
76+
|
77+
// If we can determine that part of mc.getArgument(0) is a hostname
78+
// sanitizing prefix, then we count how many placeholders occur before it
79+
// and only consider that many arguments beyond the first two as sinks.
80+
// For the `Map<String, ?>` overload this has the effect of only
81+
// considering the map values as sinks if there is at least one
82+
// placeholder in the URL before the hostname sanitizing prefix.
83+
exists(int offset |
84+
mc.urlHasPlaceholderAtOffset(i, offset) and
85+
offset < mc.getConstantUrl().(HostnameSanitizingPrefix).getOffset()
86+
)
87+
or
88+
// If we cannot determine that part of mc.getArgument(0) is a hostname
89+
// sanitizing prefix, but it is a compile time constant and we can get
90+
// its string value, then we count how many placeholders occur in it
91+
// and only consider that many arguments beyond the first two as sinks.
92+
// For the `Map<String, ?>` overload this has the effect of only
93+
// considering the map values as sinks if there is at least one
94+
// placeholder in the URL.
95+
not mc.getConstantUrl() instanceof HostnameSanitizingPrefix and
96+
mc.urlHasPlaceholderAtOffset(i, _)
97+
or
98+
// If we cannot determine the string value of mc.getArgument(0), then we
99+
// conservatively consider all arguments as sinks.
100+
not exists(mc.getConstantUrl().getStringValue())
101+
)
102+
}
103+
}

java/ql/lib/semmle/code/java/security/RequestForgery.qll

+12-7
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,17 @@ abstract class RequestForgerySanitizer extends DataFlow::Node { }
6363

6464
private class PrimitiveSanitizer extends RequestForgerySanitizer instanceof SimpleTypeSanitizer { }
6565

66-
private class HostnameSanitizingPrefix extends InterestingPrefix {
66+
/**
67+
* A string constant that contains a prefix which looks like when it is prepended to untrusted
68+
* input, it will restrict the host or entity addressed.
69+
*
70+
* For example, anything containing `?` or `#`, or a slash that doesn't appear to be a protocol
71+
* specifier (e.g. `http://` is not sanitizing), or specifically the string "/".
72+
*/
73+
class HostnameSanitizingPrefix extends InterestingPrefix {
6774
int offset;
6875

6976
HostnameSanitizingPrefix() {
70-
// Matches strings that look like when prepended to untrusted input, they will restrict
71-
// the host or entity addressed: for example, anything containing `?` or `#`, or a slash that
72-
// doesn't appear to be a protocol specifier (e.g. `http://` is not sanitizing), or specifically
73-
// the string "/".
7477
exists(this.getStringValue().regexpFind("([?#]|[^?#:/\\\\][/\\\\])|^/$", 0, offset))
7578
}
7679

@@ -81,8 +84,10 @@ private class HostnameSanitizingPrefix extends InterestingPrefix {
8184
* A value that is the result of prepending a string that prevents any value from controlling the
8285
* host of a URL.
8386
*/
84-
private class HostnameSantizer extends RequestForgerySanitizer {
85-
HostnameSantizer() { this.asExpr() = any(HostnameSanitizingPrefix hsp).getAnAppendedExpression() }
87+
private class HostnameSanitizer extends RequestForgerySanitizer {
88+
HostnameSanitizer() {
89+
this.asExpr() = any(HostnameSanitizingPrefix hsp).getAnAppendedExpression()
90+
}
8691
}
8792

8893
/**

java/ql/test/query-tests/security/CWE-918/SpringSSRF.java

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.net.http.HttpRequest;
1414
import java.net.Proxy.Type;
1515
import java.io.InputStream;
16+
import java.util.Map;
1617

1718
import org.apache.http.client.methods.HttpGet;
1819
import javax.servlet.ServletException;
@@ -32,6 +33,14 @@ protected void doGet(HttpServletRequest request2, HttpServletResponse response2)
3233
restTemplate.exchange(fooResourceUrl, HttpMethod.POST, request, String.class); // $ SSRF
3334
restTemplate.execute(fooResourceUrl, HttpMethod.POST, null, null, "test"); // $ SSRF
3435
restTemplate.getForObject(fooResourceUrl, String.class, "test"); // $ SSRF
36+
restTemplate.getForObject("http://{foo}", String.class, fooResourceUrl); // $ SSRF
37+
restTemplate.getForObject("http://{foo}/a/b", String.class, fooResourceUrl); // $ SSRF
38+
restTemplate.getForObject("http://safe.com/{foo}", String.class, fooResourceUrl); // not bad - the tainted value does not affect the host
39+
restTemplate.getForObject("http://{foo}", String.class, "safe.com", fooResourceUrl); // not bad - the tainted value is unused
40+
restTemplate.getForObject("http://{foo}", String.class, Map.of("foo", fooResourceUrl)); // $ SSRF
41+
restTemplate.getForObject("http://safe.com/{foo}", String.class, Map.of("foo", fooResourceUrl)); // not bad - the tainted value does not affect the host
42+
restTemplate.getForObject("http://{foo}", String.class, Map.of("foo", "safe.com", "unused", fooResourceUrl)); // $ SPURIOUS: SSRF // not bad - the key for the tainted value is unused
43+
restTemplate.getForObject("http://{foo}", String.class, Map.of("foo", "safe.com", fooResourceUrl, "unused")); // not bad - the tainted value is in a map key
3544
restTemplate.patchForObject(fooResourceUrl, new String("object"), String.class, "hi"); // $ SSRF
3645
restTemplate.postForEntity(new URI(fooResourceUrl), new String("object"), String.class); // $ SSRF
3746
restTemplate.postForLocation(fooResourceUrl, new String("object")); // $ SSRF

0 commit comments

Comments
 (0)