Skip to content

Commit f0d5520

Browse files
author
p0wn4j
committed
Add Spring URL Redirect ResponseEntity sink
Copyedit qhelp
1 parent 46fbb2a commit f0d5520

File tree

6 files changed

+241
-38
lines changed

6 files changed

+241
-38
lines changed

java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,29 @@ public ModelAndView bad4(String redirectUrl) {
3434
}
3535

3636
@GetMapping("url5")
37+
public ResponseEntity<Void> bad5(String redirectUrl) {
38+
return ResponseEntity.status(HttpStatus.FOUND)
39+
.location(URI.create(redirectUrl))
40+
.build();
41+
}
42+
43+
@GetMapping("url6")
44+
public ResponseEntity<Void> bad6(String redirectUrl) {
45+
HttpHeaders httpHeaders = new HttpHeaders();
46+
httpHeaders.setLocation(URI.create(redirectUrl));
47+
48+
return new ResponseEntity<>(httpHeaders, HttpStatus.SEE_OTHER);
49+
}
50+
51+
@GetMapping("url7")
52+
public ResponseEntity<Void> bad7(String redirectUrl) {
53+
HttpHeaders httpHeaders = new HttpHeaders();
54+
httpHeaders.add("Location", redirectUrl);
55+
56+
return ResponseEntity.status(HttpStatus.SEE_OTHER).headers(httpHeaders).build();
57+
}
58+
59+
@GetMapping("url8")
3760
public RedirectView good1(String redirectUrl) {
3861
RedirectView rv = new RedirectView();
3962
if (redirectUrl.startsWith(VALID_REDIRECT)){

java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qhelp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ redirects on the server; then choose from that list based on the user input prov
2121
<example>
2222

2323
<p>The following examples show the bad case and the good case respectively.
24-
In <code>bad1</code> method and <code>bad2</code> method and <code>bad3</code> method and
25-
<code>bad4</code> method, shows an HTTP request parameter being used directly in a URL redirect
26-
without validating the input, which facilitates phishing attacks. In <code>good1</code> method,
27-
shows how to solve this problem by verifying whether the user input is a known fixed string beginning.
24+
The <code>bad</code> methods show an HTTP request parameter being used directly
25+
in a URL redirect without validating the input, which facilitates phishing attacks.
26+
In the <code>good1</code> method, it is shown how to solve this problem by verifying whether
27+
the user input is a known fixed string beginning.
2828
</p>
2929

3030
<sample src="SpringUrlRedirect.java" />
@@ -33,5 +33,6 @@ shows how to solve this problem by verifying whether the user input is a known f
3333
<references>
3434
<li>A Guide To Spring Redirects: <a href="https://www.baeldung.com/spring-redirect-and-forward">Spring Redirects</a>.</li>
3535
<li>Url redirection - attack and defense: <a href="https://www.virtuesecurity.com/kb/url-redirection-attack-and-defense/">Url Redirection</a>.</li>
36+
<li>How to redirect to an external URL from Spring Boot REST Controller (Post/Redirect/Get pattern)?: <a href="https://fullstackdeveloper.guru/2021/03/12/how-to-redirect-to-an-external-url-from-spring-boot-rest-controller/">ResponseEntity Redirection</a>.</li>
3637
</references>
3738
</qhelp>

java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class SpringUrlRedirectFlowConfig extends TaintTracking::Configuration {
3434

3535
override predicate isSink(DataFlow::Node sink) { sink instanceof SpringUrlRedirectSink }
3636

37+
override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
38+
springUrlRedirectTaintStep(fromNode, toNode)
39+
}
40+
3741
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
3842
guard instanceof StartsWithSanitizer
3943
}
@@ -57,6 +61,8 @@ class SpringUrlRedirectFlowConfig extends TaintTracking::Configuration {
5761
not ma.getArgument(0).(CompileTimeConstantExpr).getStringValue().regexpMatch("^%s.*")
5862
)
5963
)
64+
or
65+
nonLocationHeaderSanitizer(node)
6066
}
6167
}
6268

java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@ class RedirectAppendCall extends MethodAccess {
3535
}
3636

3737
/** A URL redirection sink from spring controller method. */
38-
class SpringUrlRedirectSink extends DataFlow::Node {
39-
SpringUrlRedirectSink() {
38+
abstract class SpringUrlRedirectSink extends DataFlow::Node { }
39+
40+
/**
41+
* A sink for URL Redirection via the Spring View classes.
42+
*/
43+
private class SpringViewUrlRedirectSink extends SpringUrlRedirectSink {
44+
SpringViewUrlRedirectSink() {
4045
exists(RedirectBuilderExpr rbe |
4146
rbe.getRightOperand() = this.asExpr() and
4247
any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable())
@@ -71,3 +76,65 @@ class SpringUrlRedirectSink extends DataFlow::Node {
7176
)
7277
}
7378
}
79+
80+
/**
81+
* A sink for URL Redirection via the `ResponseEntity` class.
82+
*/
83+
private class SpringResponseEntityUrlRedirectSink extends SpringUrlRedirectSink {
84+
SpringResponseEntityUrlRedirectSink() {
85+
// Find `new ResponseEntity<>(httpHeaders, ...)` or
86+
// `new ResponseEntity<>(..., httpHeaders, ...)` sinks
87+
exists(ClassInstanceExpr cie, Argument argument |
88+
cie.getConstructedType() instanceof SpringResponseEntity and
89+
argument.getType() instanceof SpringHttpHeaders and
90+
argument = cie.getArgument([0, 1]) and
91+
this.asExpr() = argument
92+
)
93+
or
94+
// Find `ResponseEntity.status(...).headers(taintHeaders).build()` or
95+
// `ResponseEntity.status(...).location(URI.create(taintURL)).build()` sinks
96+
exists(MethodAccess ma |
97+
ma.getMethod()
98+
.getDeclaringType()
99+
.hasQualifiedName("org.springframework.http",
100+
"ResponseEntity<>$HeadersBuilder<BodyBuilder>") and
101+
ma.getMethod().getName() in ["headers", "location"] and
102+
this.asExpr() = ma.getArgument(0)
103+
)
104+
}
105+
}
106+
107+
private class HttpHeadersMethodAccess extends MethodAccess {
108+
HttpHeadersMethodAccess() { this.getMethod().getDeclaringType() instanceof SpringHttpHeaders }
109+
}
110+
111+
private class HttpHeadersAddSetMethodAccess extends HttpHeadersMethodAccess {
112+
HttpHeadersAddSetMethodAccess() { this.getMethod().getName() in ["add", "set"] }
113+
}
114+
115+
private class HttpHeadersSetLocationMethodAccess extends HttpHeadersMethodAccess {
116+
HttpHeadersSetLocationMethodAccess() { this.getMethod().hasName("setLocation") }
117+
}
118+
119+
/**
120+
* Holds if `fromNode` to `toNode` is a dataflow step from a tainted argument to
121+
* a `HttpHeaders` instance qualifier, i.e. `httpHeaders.setLocation(tainted)`.
122+
*/
123+
predicate springUrlRedirectTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
124+
exists(HttpHeadersSetLocationMethodAccess ma |
125+
fromNode.asExpr() = ma.getArgument(0) and
126+
toNode.asExpr() = ma.getQualifier()
127+
)
128+
}
129+
130+
/**
131+
* A sanitizer to exclude the cases where the `HttpHeaders.add` or `HttpHeaders.set`
132+
* methods are called with a HTTP header other than "Location".
133+
* E.g: `httpHeaders.add("X-Some-Header", taintedUrlString)`
134+
*/
135+
predicate nonLocationHeaderSanitizer(DataFlow::Node node) {
136+
exists(HttpHeadersAddSetMethodAccess ma, Argument firstArg | node.asExpr() = ma.getArgument(1) |
137+
firstArg = ma.getArgument(0) and
138+
not firstArg.(CompileTimeConstantExpr).getStringValue().matches("Location")
139+
)
140+
}

0 commit comments

Comments
 (0)