Skip to content

Commit b69eba9

Browse files
committed
Add check for Spring redirect
1 parent 1ce31ec commit b69eba9

File tree

5 files changed

+261
-3
lines changed

5 files changed

+261
-3
lines changed

java/ql/src/experimental/Security/CWE/CWE-625/PermissiveDotRegex.ql

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import semmle.code.java.dataflow.FlowSources
1818
import semmle.code.java.security.UrlRedirect
1919
import DataFlow::PathGraph
2020
import Regex
21+
import SpringUrlRedirect
2122

2223
/** Source model of remote flow source with servlets. */
2324
private class GetServletUriSource extends SourceModelCsv {
@@ -51,6 +52,30 @@ private class UrlFilterSink extends SinkModelCsv {
5152
}
5253
}
5354

55+
/** A Spring framework annotation indicating remote uri user input. */
56+
class SpringUriInputAnnotation extends Annotation {
57+
SpringUriInputAnnotation() {
58+
exists(AnnotationType a |
59+
a = this.getType() and
60+
a.getPackage().getName() = "org.springframework.web.bind.annotation"
61+
|
62+
(
63+
a.hasName("PathVariable") or
64+
a.hasName("RequestParam")
65+
)
66+
)
67+
}
68+
}
69+
70+
class SpringUriInputParameterSource extends DataFlow::Node {
71+
SpringUriInputParameterSource() {
72+
this.asParameter() =
73+
any(SpringRequestMappingParameter srmp |
74+
srmp.getAnAnnotation() instanceof SpringUriInputAnnotation
75+
)
76+
}
77+
}
78+
5479
/**
5580
* `.` without a `\` prefix, which is likely not a character literal in regex
5681
*/
@@ -121,7 +146,10 @@ class PermissiveDotRegexConfig extends DataFlow::Configuration {
121146
class MatchRegexConfiguration extends TaintTracking::Configuration {
122147
MatchRegexConfiguration() { this = "PermissiveDotRegex::MatchRegexConfiguration" }
123148

124-
override predicate isSource(DataFlow::Node source) { sourceNode(source, "uri-path") }
149+
override predicate isSource(DataFlow::Node source) {
150+
sourceNode(source, "uri-path") or // Servlet uri source
151+
source instanceof SpringUriInputParameterSource // Spring uri source
152+
}
125153

126154
override predicate isSink(DataFlow::Node sink) {
127155
sink instanceof MatchRegexSink and
@@ -140,7 +168,8 @@ class MatchRegexConfiguration extends TaintTracking::Configuration {
140168
(
141169
DataFlow::exprNode(se) instanceof UrlRedirectSink or
142170
sinkNode(DataFlow::exprNode(se), "url-dispatch") or
143-
sinkNode(DataFlow::exprNode(se), "url-filter")
171+
sinkNode(DataFlow::exprNode(se), "url-filter") or
172+
DataFlow::exprNode(se) instanceof SpringUrlRedirectSink
144173
) and
145174
guard.controls(se.getBasicBlock(), true)
146175
)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/** Provides methods related to Spring URL redirect from /src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll. */
2+
3+
private import java
4+
private import semmle.code.java.dataflow.FlowSources
5+
6+
/**
7+
* A concatenate expression using the string `redirect:` or `ajaxredirect:` or `forward:` on the left.
8+
*
9+
* E.g: `"redirect:" + redirectUrl`
10+
*/
11+
class RedirectBuilderExpr extends AddExpr {
12+
RedirectBuilderExpr() {
13+
this.getLeftOperand().(CompileTimeConstantExpr).getStringValue() in [
14+
"redirect:", "ajaxredirect:", "forward:"
15+
]
16+
}
17+
}
18+
19+
/**
20+
* A call to `StringBuilder.append` or `StringBuffer.append` method, and the parameter value is
21+
* `"redirect:"` or `"ajaxredirect:"` or `"forward:"`.
22+
*
23+
* E.g: `StringBuilder.append("redirect:")`
24+
*/
25+
class RedirectAppendCall extends MethodAccess {
26+
RedirectAppendCall() {
27+
this.getMethod().hasName("append") and
28+
this.getMethod().getDeclaringType() instanceof StringBuildingType and
29+
this.getArgument(0).(CompileTimeConstantExpr).getStringValue() in [
30+
"redirect:", "ajaxredirect:", "forward:"
31+
]
32+
}
33+
}
34+
35+
/** A URL redirection sink from spring controller method. */
36+
abstract class SpringUrlRedirectSink extends DataFlow::Node { }
37+
38+
/**
39+
* A sink for URL Redirection via the Spring View classes.
40+
*/
41+
private class SpringViewUrlRedirectSink extends SpringUrlRedirectSink {
42+
SpringViewUrlRedirectSink() {
43+
// Hardcoded redirect such as "redirect:login"
44+
this.asExpr()
45+
.(CompileTimeConstantExpr)
46+
.getStringValue()
47+
.indexOf(["redirect:", "ajaxredirect:", "forward:"]) = 0 and
48+
any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable())
49+
or
50+
exists(RedirectBuilderExpr rbe |
51+
rbe.getRightOperand() = this.asExpr() and
52+
any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable())
53+
)
54+
or
55+
exists(MethodAccess ma, RedirectAppendCall rac |
56+
DataFlow2::localExprFlow(rac.getQualifier(), ma.getQualifier()) and
57+
ma.getMethod().hasName("append") and
58+
ma.getArgument(0) = this.asExpr() and
59+
any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable())
60+
)
61+
or
62+
exists(MethodAccess ma |
63+
ma.getMethod().hasName("setUrl") and
64+
ma.getMethod()
65+
.getDeclaringType()
66+
.hasQualifiedName("org.springframework.web.servlet.view", "AbstractUrlBasedView") and
67+
ma.getArgument(0) = this.asExpr()
68+
)
69+
or
70+
exists(ClassInstanceExpr cie |
71+
cie.getConstructedType()
72+
.hasQualifiedName("org.springframework.web.servlet.view", "RedirectView") and
73+
cie.getArgument(0) = this.asExpr()
74+
)
75+
or
76+
exists(ClassInstanceExpr cie |
77+
cie.getConstructedType().hasQualifiedName("org.springframework.web.servlet", "ModelAndView") and
78+
exists(RedirectBuilderExpr rbe |
79+
rbe = cie.getArgument(0) and rbe.getRightOperand() = this.asExpr()
80+
)
81+
)
82+
}
83+
}
84+
85+
/**
86+
* A sink for URL Redirection via the `ResponseEntity` class.
87+
*/
88+
private class SpringResponseEntityUrlRedirectSink extends SpringUrlRedirectSink {
89+
SpringResponseEntityUrlRedirectSink() {
90+
// Find `new ResponseEntity(httpHeaders, ...)` or
91+
// `new ResponseEntity(..., httpHeaders, ...)` sinks
92+
exists(ClassInstanceExpr cie, Argument argument |
93+
cie.getConstructedType() instanceof SpringResponseEntity and
94+
argument.getType() instanceof SpringHttpHeaders and
95+
argument = cie.getArgument([0, 1]) and
96+
this.asExpr() = argument
97+
)
98+
or
99+
// Find `ResponseEntity.status(...).headers(taintHeaders).build()` or
100+
// `ResponseEntity.status(...).location(URI.create(taintURL)).build()` sinks
101+
exists(MethodAccess ma |
102+
ma.getMethod()
103+
.getDeclaringType()
104+
.hasQualifiedName("org.springframework.http", "ResponseEntity$HeadersBuilder<BodyBuilder>") and
105+
ma.getMethod().getName() in ["headers", "location"] and
106+
this.asExpr() = ma.getArgument(0)
107+
)
108+
}
109+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import org.springframework.stereotype.Controller;
2+
import org.springframework.ui.Model;
3+
import org.springframework.web.bind.annotation.GetMapping;
4+
import org.springframework.web.bind.annotation.PathVariable;
5+
import org.springframework.web.bind.annotation.RequestParam;
6+
import org.springframework.web.servlet.view.RedirectView;
7+
8+
import java.io.UnsupportedEncodingException;
9+
import java.net.URLDecoder;
10+
import java.util.regex.Matcher;
11+
import java.util.regex.Pattern;
12+
13+
@Controller
14+
public class DotRegexSpring {
15+
private static final String PROTECTED_PATTERN = "/protected/.*";
16+
private static final String CONSTRAINT_PATTERN = "/protected/xyz\\.xml";
17+
18+
@GetMapping("param")
19+
// BAD: A string with line return e.g. `/protected/%0dxyz` can bypass the path check
20+
public String withParam(@RequestParam String path, Model model) throws UnsupportedEncodingException {
21+
Pattern p = Pattern.compile(PROTECTED_PATTERN);
22+
path = decodePath(path);
23+
Matcher m = p.matcher(path);
24+
25+
if (m.matches()) {
26+
// Protected page - check access token and redirect to login page
27+
if (model.getAttribute("secAttr") == null || !model.getAttribute("secAttr").equals("secValue")) {
28+
return "redirect:login";
29+
}
30+
}
31+
// Not protected page - render content
32+
return path;
33+
}
34+
35+
@GetMapping("{path}")
36+
// BAD: A string with line return e.g. `%252Fprotected%252F%250dxyz` can bypass the path check
37+
public RedirectView withPathVariable1(@PathVariable String path, Model model) throws UnsupportedEncodingException {
38+
Pattern p = Pattern.compile(PROTECTED_PATTERN);
39+
path = decodePath(path);
40+
Matcher m = p.matcher(path);
41+
42+
if (m.matches()) {
43+
// Protected page - check access token and redirect to login page
44+
if (model.getAttribute("secAttr") == null || !model.getAttribute("secAttr").equals("secValue")) {
45+
RedirectView redirectView = new RedirectView("login", true);
46+
return redirectView;
47+
}
48+
}
49+
return null;
50+
}
51+
52+
@GetMapping("/sp/{path}")
53+
// GOOD: A string with line return e.g. `%252Fprotected%252F%250dxyz` cannot bypass the path check
54+
public String withPathVariable2(@PathVariable String path, Model model) throws UnsupportedEncodingException {
55+
Pattern p = Pattern.compile(CONSTRAINT_PATTERN);
56+
path = decodePath(path);
57+
Matcher m = p.matcher(path);
58+
59+
if (m.matches()) {
60+
// Protected page - check access token and redirect to login page
61+
if (model.getAttribute("secAttr") == null || !model.getAttribute("secAttr").equals("secValue")) {
62+
return "redirect:login";
63+
}
64+
}
65+
// Not protected page - render content
66+
return path;
67+
}
68+
69+
private String decodePath(String path) throws UnsupportedEncodingException {
70+
while (path.indexOf("%") > -1) {
71+
path = URLDecoder.decode(path, "UTF-8");
72+
}
73+
return path;
74+
}
75+
}

java/ql/test/experimental/query-tests/security/CWE-625/PermissiveDotRegex.expected

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@ edges
1515
| DotRegexServlet.java:93:19:93:39 | getPathInfo(...) : String | DotRegexServlet.java:96:25:96:30 | source |
1616
| DotRegexServlet.java:112:19:112:39 | getPathInfo(...) : String | DotRegexServlet.java:115:25:115:30 | source |
1717
| DotRegexServlet.java:133:19:133:39 | getPathInfo(...) : String | DotRegexServlet.java:136:25:136:30 | source |
18+
| DotRegexSpring.java:15:30:15:46 | PROTECTED_PATTERN : String | DotRegexSpring.java:21:31:21:47 | PROTECTED_PATTERN |
19+
| DotRegexSpring.java:15:30:15:46 | PROTECTED_PATTERN : String | DotRegexSpring.java:38:31:38:47 | PROTECTED_PATTERN |
20+
| DotRegexSpring.java:15:50:15:64 | "/protected/.*" : String | DotRegexSpring.java:15:30:15:46 | PROTECTED_PATTERN : String |
21+
| DotRegexSpring.java:20:26:20:50 | path : String | DotRegexSpring.java:22:21:22:24 | path : String |
22+
| DotRegexSpring.java:22:10:22:25 | decodePath(...) : String | DotRegexSpring.java:23:25:23:28 | path |
23+
| DotRegexSpring.java:22:21:22:24 | path : String | DotRegexSpring.java:22:10:22:25 | decodePath(...) : String |
24+
| DotRegexSpring.java:22:21:22:24 | path : String | DotRegexSpring.java:69:28:69:38 | path : String |
25+
| DotRegexSpring.java:37:40:37:64 | path : String | DotRegexSpring.java:39:21:39:24 | path : String |
26+
| DotRegexSpring.java:39:10:39:25 | decodePath(...) : String | DotRegexSpring.java:40:25:40:28 | path |
27+
| DotRegexSpring.java:39:21:39:24 | path : String | DotRegexSpring.java:39:10:39:25 | decodePath(...) : String |
28+
| DotRegexSpring.java:39:21:39:24 | path : String | DotRegexSpring.java:69:28:69:38 | path : String |
29+
| DotRegexSpring.java:54:34:54:58 | path : String | DotRegexSpring.java:56:21:56:24 | path : String |
30+
| DotRegexSpring.java:56:10:56:25 | decodePath(...) : String | DotRegexSpring.java:57:25:57:28 | path |
31+
| DotRegexSpring.java:56:21:56:24 | path : String | DotRegexSpring.java:56:10:56:25 | decodePath(...) : String |
32+
| DotRegexSpring.java:56:21:56:24 | path : String | DotRegexSpring.java:69:28:69:38 | path : String |
33+
| DotRegexSpring.java:69:28:69:38 | path : String | DotRegexSpring.java:71:29:71:32 | path : String |
34+
| DotRegexSpring.java:69:28:69:38 | path : String | DotRegexSpring.java:73:10:73:13 | path : String |
35+
| DotRegexSpring.java:71:11:71:42 | decode(...) : String | DotRegexSpring.java:71:29:71:32 | path : String |
36+
| DotRegexSpring.java:71:11:71:42 | decode(...) : String | DotRegexSpring.java:73:10:73:13 | path : String |
37+
| DotRegexSpring.java:71:29:71:32 | path : String | DotRegexSpring.java:71:11:71:42 | decode(...) : String |
1838
nodes
1939
| DotRegexFilter.java:16:30:16:46 | PROTECTED_PATTERN : String | semmle.label | PROTECTED_PATTERN : String |
2040
| DotRegexFilter.java:16:50:16:64 | "/protected/.*" : String | semmle.label | "/protected/.*" : String |
@@ -43,10 +63,35 @@ nodes
4363
| DotRegexServlet.java:115:25:115:30 | source | semmle.label | source |
4464
| DotRegexServlet.java:133:19:133:39 | getPathInfo(...) : String | semmle.label | getPathInfo(...) : String |
4565
| DotRegexServlet.java:136:25:136:30 | source | semmle.label | source |
66+
| DotRegexSpring.java:15:30:15:46 | PROTECTED_PATTERN : String | semmle.label | PROTECTED_PATTERN : String |
67+
| DotRegexSpring.java:15:50:15:64 | "/protected/.*" : String | semmle.label | "/protected/.*" : String |
68+
| DotRegexSpring.java:20:26:20:50 | path : String | semmle.label | path : String |
69+
| DotRegexSpring.java:21:31:21:47 | PROTECTED_PATTERN | semmle.label | PROTECTED_PATTERN |
70+
| DotRegexSpring.java:22:10:22:25 | decodePath(...) : String | semmle.label | decodePath(...) : String |
71+
| DotRegexSpring.java:22:21:22:24 | path : String | semmle.label | path : String |
72+
| DotRegexSpring.java:23:25:23:28 | path | semmle.label | path |
73+
| DotRegexSpring.java:37:40:37:64 | path : String | semmle.label | path : String |
74+
| DotRegexSpring.java:38:31:38:47 | PROTECTED_PATTERN | semmle.label | PROTECTED_PATTERN |
75+
| DotRegexSpring.java:39:10:39:25 | decodePath(...) : String | semmle.label | decodePath(...) : String |
76+
| DotRegexSpring.java:39:21:39:24 | path : String | semmle.label | path : String |
77+
| DotRegexSpring.java:40:25:40:28 | path | semmle.label | path |
78+
| DotRegexSpring.java:54:34:54:58 | path : String | semmle.label | path : String |
79+
| DotRegexSpring.java:56:10:56:25 | decodePath(...) : String | semmle.label | decodePath(...) : String |
80+
| DotRegexSpring.java:56:21:56:24 | path : String | semmle.label | path : String |
81+
| DotRegexSpring.java:57:25:57:28 | path | semmle.label | path |
82+
| DotRegexSpring.java:69:28:69:38 | path : String | semmle.label | path : String |
83+
| DotRegexSpring.java:71:11:71:42 | decode(...) : String | semmle.label | decode(...) : String |
84+
| DotRegexSpring.java:71:29:71:32 | path : String | semmle.label | path : String |
85+
| DotRegexSpring.java:73:10:73:13 | path : String | semmle.label | path : String |
4686
subpaths
87+
| DotRegexSpring.java:22:21:22:24 | path : String | DotRegexSpring.java:69:28:69:38 | path : String | DotRegexSpring.java:73:10:73:13 | path : String | DotRegexSpring.java:22:10:22:25 | decodePath(...) : String |
88+
| DotRegexSpring.java:39:21:39:24 | path : String | DotRegexSpring.java:69:28:69:38 | path : String | DotRegexSpring.java:73:10:73:13 | path : String | DotRegexSpring.java:39:10:39:25 | decodePath(...) : String |
89+
| DotRegexSpring.java:56:21:56:24 | path : String | DotRegexSpring.java:69:28:69:38 | path : String | DotRegexSpring.java:73:10:73:13 | path : String | DotRegexSpring.java:56:10:56:25 | decodePath(...) : String |
4790
#select
4891
| DotRegexFilter.java:32:25:32:30 | source | DotRegexFilter.java:29:19:29:43 | getPathInfo(...) : String | DotRegexFilter.java:32:25:32:30 | source | Potentially authentication bypass due to $@. | DotRegexFilter.java:29:19:29:43 | getPathInfo(...) | user-provided value |
4992
| DotRegexServlet.java:22:25:22:30 | source | DotRegexServlet.java:19:19:19:39 | getPathInfo(...) : String | DotRegexServlet.java:22:25:22:30 | source | Potentially authentication bypass due to $@. | DotRegexServlet.java:19:19:19:39 | getPathInfo(...) | user-provided value |
5093
| DotRegexServlet.java:59:21:59:26 | source | DotRegexServlet.java:57:19:57:41 | getRequestURI(...) : String | DotRegexServlet.java:59:21:59:26 | source | Potentially authentication bypass due to $@. | DotRegexServlet.java:57:19:57:41 | getRequestURI(...) | user-provided value |
5194
| DotRegexServlet.java:77:56:77:61 | source | DotRegexServlet.java:75:19:75:39 | getPathInfo(...) : String | DotRegexServlet.java:77:56:77:61 | source | Potentially authentication bypass due to $@. | DotRegexServlet.java:75:19:75:39 | getPathInfo(...) | user-provided value |
5295
| DotRegexServlet.java:115:25:115:30 | source | DotRegexServlet.java:112:19:112:39 | getPathInfo(...) : String | DotRegexServlet.java:115:25:115:30 | source | Potentially authentication bypass due to $@. | DotRegexServlet.java:112:19:112:39 | getPathInfo(...) | user-provided value |
96+
| DotRegexSpring.java:23:25:23:28 | path | DotRegexSpring.java:20:26:20:50 | path : String | DotRegexSpring.java:23:25:23:28 | path | Potentially authentication bypass due to $@. | DotRegexSpring.java:20:26:20:50 | path | user-provided value |
97+
| DotRegexSpring.java:40:25:40:28 | path | DotRegexSpring.java:37:40:37:64 | path : String | DotRegexSpring.java:40:25:40:28 | path | Potentially authentication bypass due to $@. | DotRegexSpring.java:37:40:37:64 | path | user-provided value |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4
1+
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/springframework-5.3.8

0 commit comments

Comments
 (0)