Skip to content

Commit 969b0cf

Browse files
committed
Add SSRF sinks for uriVariables arguments of more methods on Spring RestTemplate
1 parent 0c358ac commit 969b0cf

File tree

3 files changed

+485
-137
lines changed

3 files changed

+485
-137
lines changed

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

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,50 +31,74 @@ class SpringWebClient extends Interface {
3131
}
3232
}
3333

34-
/** The method `getForObject` on `org.springframework.web.reactive.function.client.RestTemplate`. */
35-
class SpringRestTemplateGetForObjectMethod extends Method {
36-
SpringRestTemplateGetForObjectMethod() {
34+
/**
35+
* A method on `org.springframework.web.reactive.function.client.RestTemplate`
36+
* which has a parameter `uriVariables` (which can have type `Object..` or
37+
* `Map<String, ?>`) which contains variables to be expanded into the URL
38+
* template in parameter 0.
39+
*/
40+
private class SpringRestTemplateMethodWithUriVariablesParameter extends Method {
41+
int pos;
42+
43+
SpringRestTemplateMethodWithUriVariablesParameter() {
3744
this.getDeclaringType() instanceof SpringRestTemplate and
38-
this.hasName("getForObject")
45+
(
46+
this.hasName("delete") and pos = 1
47+
or
48+
this.hasName("exchange") and pos = 4
49+
or
50+
this.hasName("execute") and pos = 4
51+
or
52+
this.hasName("getForEntity") and pos = 2
53+
or
54+
this.hasName("getForObject") and pos = 2
55+
or
56+
this.hasName("headForHeaders") and pos = 1
57+
or
58+
this.hasName("optionsForAllow") and pos = 1
59+
or
60+
this.hasName("patchForObject") and pos = 3
61+
or
62+
this.hasName("postForEntity") and pos = 3
63+
or
64+
this.hasName("postForLocation") and pos = 2
65+
or
66+
this.hasName("postForObject") and pos = 3
67+
or
68+
this.hasName("put") and pos = 2
69+
)
3970
}
40-
}
4171

42-
/** A call to the method `getForObject` on `org.springframework.web.reactive.function.client.RestTemplate`. */
43-
class SpringRestTemplateGetForObjectMethodCall extends MethodCall {
44-
SpringRestTemplateGetForObjectMethodCall() {
45-
this.getMethod() instanceof SpringRestTemplateGetForObjectMethod
46-
}
72+
int getUriVariablesPosition() { result = pos }
73+
}
4774

48-
/** Gets the first argument, if it is a compile time constant. */
49-
CompileTimeConstantExpr getConstantUrl() { result = this.getArgument(0) }
75+
/** Gets the first argument, if it is a compile time constant. */
76+
pragma[inline]
77+
private CompileTimeConstantExpr getConstantUrl(MethodCall mc) { result = mc.getArgument(0) }
5078

51-
/**
52-
* Holds if the first argument is a compile time constant and it has a
53-
* placeholder at offset `offset`, and there are `idx` placeholders that
54-
* appear before it.
55-
*/
56-
predicate urlHasPlaceholderAtOffset(int idx, int offset) {
57-
exists(
58-
this.getConstantUrl()
59-
.getStringValue()
60-
.replaceAll("\\{", " ")
61-
.replaceAll("\\}", " ")
62-
.regexpFind("\\{[^}]*\\}", idx, offset)
63-
)
64-
}
79+
pragma[inline]
80+
private predicate urlHasPlaceholderAtOffset(MethodCall mc, int idx, int offset) {
81+
exists(
82+
getConstantUrl(mc)
83+
.getStringValue()
84+
.replaceAll("\\{", " ")
85+
.replaceAll("\\}", " ")
86+
.regexpFind("\\{[^}]*\\}", idx, offset)
87+
)
6588
}
6689

6790
private class SpringWebClientRestTemplateGetForObject extends RequestForgerySink {
6891
SpringWebClientRestTemplateGetForObject() {
69-
exists(SpringRestTemplateGetForObjectMethodCall mc, int i |
92+
exists(SpringRestTemplateMethodWithUriVariablesParameter m, MethodCall mc, int i |
7093
// Note that the first argument is modeled as a request forgery sink
7194
// separately. This model is for arguments beyond the first two. There
7295
// are two relevant overloads, one with third parameter type `Object...`
7396
// and one with third parameter type `Map<String, ?>`. For the latter we
7497
// cannot deal with MapValue content easily but there is a default
7598
// implicit taint read at sinks that will catch it.
99+
mc.getMethod() = m and
76100
i >= 0 and
77-
this.asExpr() = mc.getArgument(i + 2)
101+
this.asExpr() = mc.getArgument(m.getUriVariablesPosition() + i)
78102
|
79103
// If we can determine that part of mc.getArgument(0) is a hostname
80104
// sanitizing prefix, then we count how many placeholders occur before it
@@ -83,8 +107,8 @@ private class SpringWebClientRestTemplateGetForObject extends RequestForgerySink
83107
// considering the map values as sinks if there is at least one
84108
// placeholder in the URL before the hostname sanitizing prefix.
85109
exists(int offset |
86-
mc.urlHasPlaceholderAtOffset(i, offset) and
87-
offset < mc.getConstantUrl().(HostnameSanitizingPrefix).getOffset()
110+
urlHasPlaceholderAtOffset(mc, i, offset) and
111+
offset < getConstantUrl(mc).(HostnameSanitizingPrefix).getOffset()
88112
)
89113
or
90114
// If we cannot determine that part of mc.getArgument(0) is a hostname
@@ -94,12 +118,12 @@ private class SpringWebClientRestTemplateGetForObject extends RequestForgerySink
94118
// For the `Map<String, ?>` overload this has the effect of only
95119
// considering the map values as sinks if there is at least one
96120
// placeholder in the URL.
97-
not mc.getConstantUrl() instanceof HostnameSanitizingPrefix and
98-
mc.urlHasPlaceholderAtOffset(i, _)
121+
not getConstantUrl(mc) instanceof HostnameSanitizingPrefix and
122+
urlHasPlaceholderAtOffset(mc, i, _)
99123
or
100124
// If we cannot determine the string value of mc.getArgument(0), then we
101125
// conservatively consider all arguments as sinks.
102-
not exists(mc.getConstantUrl().getStringValue())
126+
not exists(getConstantUrl(mc).getStringValue())
103127
)
104128
}
105129
}

0 commit comments

Comments
 (0)