Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* URI template variables of all Spring `RestTemplate` methods are now considered as request forgery sinks. Previously only the `getForObject` method was considered. This may lead to more alerts for the query `java/ssrf`.
111 changes: 70 additions & 41 deletions java/ql/lib/semmle/code/java/frameworks/spring/SpringWebClient.qll
Original file line number Diff line number Diff line change
Expand Up @@ -31,50 +31,79 @@ class SpringWebClient extends Interface {
}
}

/** The method `getForObject` on `org.springframework.web.reactive.function.client.RestTemplate`. */
class SpringRestTemplateGetForObjectMethod extends Method {
SpringRestTemplateGetForObjectMethod() {
/**
* A method on `org.springframework.web.client.RestTemplate`
* which has a parameter `uriVariables` (which can have type `Object..` or
* `Map<String, ?>`) which contains variables to be expanded into the URL
* template in parameter 0.
*/
private class SpringRestTemplateMethodWithUriVariablesParameter extends Method {
int pos;

SpringRestTemplateMethodWithUriVariablesParameter() {
this.getDeclaringType() instanceof SpringRestTemplate and
this.hasName("getForObject")
(
this.hasName("delete") and pos = 1
or
this.hasName("exchange") and pos = 4
or
this.hasName("execute") and pos = 4
or
this.hasName("getForEntity") and pos = 2
or
this.hasName("getForObject") and pos = 2
or
this.hasName("headForHeaders") and pos = 1
or
this.hasName("optionsForAllow") and pos = 1
or
this.hasName("patchForObject") and pos = 3
or
this.hasName("postForEntity") and pos = 3
or
this.hasName("postForLocation") and pos = 2
or
this.hasName("postForObject") and pos = 3
or
this.hasName("put") and pos = 2
)
}
}

/** A call to the method `getForObject` on `org.springframework.web.reactive.function.client.RestTemplate`. */
class SpringRestTemplateGetForObjectMethodCall extends MethodCall {
SpringRestTemplateGetForObjectMethodCall() {
this.getMethod() instanceof SpringRestTemplateGetForObjectMethod
}
int getUriVariablesPosition() { result = pos }
}

/** Gets the first argument, if it is a compile time constant. */
CompileTimeConstantExpr getConstantUrl() { result = this.getArgument(0) }
/** Gets the first argument of `mc`, if it is a compile-time constant. */
pragma[inline]
private CompileTimeConstantExpr getConstantUrl(MethodCall mc) { result = mc.getArgument(0) }

/**
* Holds if the first argument is a compile time constant and it has a
* placeholder at offset `offset`, and there are `idx` placeholders that
* appear before it.
*/
predicate urlHasPlaceholderAtOffset(int idx, int offset) {
exists(
this.getConstantUrl()
.getStringValue()
.replaceAll("\\{", " ")
.replaceAll("\\}", " ")
.regexpFind("\\{[^}]*\\}", idx, offset)
)
}
/**
* Holds if the first argument of `mc` is a compile-time constant URL template
* which has its `idx`-th placeholder at the offset `offset`.
*/
pragma[inline]
private predicate urlHasPlaceholderAtOffset(MethodCall mc, int idx, int offset) {
exists(
getConstantUrl(mc)
.getStringValue()
.replaceAll("\\{", " ")
.replaceAll("\\}", " ")
.regexpFind("\\{[^}]*\\}", idx, offset)
)
}

private class SpringWebClientRestTemplateGetForObject extends RequestForgerySink {
SpringWebClientRestTemplateGetForObject() {
exists(SpringRestTemplateGetForObjectMethodCall mc, int i |
// Note that the first argument is modeled as a request forgery sink
// separately. This model is for arguments beyond the first two. There
// are two relevant overloads, one with third parameter type `Object...`
// and one with third parameter type `Map<String, ?>`. For the latter we
// cannot deal with MapValue content easily but there is a default
// implicit taint read at sinks that will catch it.
private class SpringWebClientRestTemplateUriVariable extends RequestForgerySink {
SpringWebClientRestTemplateUriVariable() {
exists(SpringRestTemplateMethodWithUriVariablesParameter m, MethodCall mc, int i |
// Note that the first argument of `m` is modeled as a request forgery
// sink separately. This model is for arguments corresponding to the
// `uriVariables` parameter. There are always two relevant overloads, one
// with parameter type `Object...` and one with parameter type
// `Map<String, ?>`. For the latter we cannot deal with MapValue content
// easily but there is a default implicit taint read at sinks that will
// catch it.
mc.getMethod() = m and
i >= 0 and
this.asExpr() = mc.getArgument(i + 2)
this.asExpr() = mc.getArgument(m.getUriVariablesPosition() + i)
|
// If we can determine that part of mc.getArgument(0) is a hostname
// sanitizing prefix, then we count how many placeholders occur before it
Expand All @@ -83,8 +112,8 @@ private class SpringWebClientRestTemplateGetForObject extends RequestForgerySink
// considering the map values as sinks if there is at least one
// placeholder in the URL before the hostname sanitizing prefix.
exists(int offset |
mc.urlHasPlaceholderAtOffset(i, offset) and
offset < mc.getConstantUrl().(HostnameSanitizingPrefix).getOffset()
urlHasPlaceholderAtOffset(mc, i, offset) and
offset < getConstantUrl(mc).(HostnameSanitizingPrefix).getOffset()
)
or
// If we cannot determine that part of mc.getArgument(0) is a hostname
Expand All @@ -94,12 +123,12 @@ private class SpringWebClientRestTemplateGetForObject extends RequestForgerySink
// For the `Map<String, ?>` overload this has the effect of only
// considering the map values as sinks if there is at least one
// placeholder in the URL.
not mc.getConstantUrl() instanceof HostnameSanitizingPrefix and
mc.urlHasPlaceholderAtOffset(i, _)
not getConstantUrl(mc) instanceof HostnameSanitizingPrefix and
urlHasPlaceholderAtOffset(mc, i, _)
or
// If we cannot determine the string value of mc.getArgument(0), then we
// conservatively consider all arguments as sinks.
not exists(mc.getConstantUrl().getStringValue())
not exists(getConstantUrl(mc).getStringValue())
)
}
}
Loading