Skip to content

Commit f231bad

Browse files
committed
Tighten Spring redirect sinks
1 parent 3a4deee commit f231bad

3 files changed

Lines changed: 153 additions & 73 deletions

File tree

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

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import semmle.code.java.frameworks.ApacheHttp
99
private import semmle.code.java.dataflow.ExternalFlow
1010
private import semmle.code.java.dataflow.FlowSinks
1111
private import semmle.code.java.dataflow.StringPrefixes
12+
private import semmle.code.java.dataflow.TaintTracking
1213
private import semmle.code.java.frameworks.JaxWS
1314
private import semmle.code.java.frameworks.spring.SpringController
1415
private import semmle.code.java.security.RequestForgery
@@ -52,14 +53,26 @@ private class ApacheUrlRedirectSink extends UrlRedirectSink {
5253
}
5354
}
5455

55-
/**
56-
* An expression appended to a Spring `"redirect:"` view-name prefix from a request handler or a
57-
* helper called by one.
58-
*/
56+
/** An expression appended to a Spring `"redirect:"` view-name returned by a request handler. */
5957
private class SpringUrlRedirectPrefixSink extends UrlRedirectSink {
6058
SpringUrlRedirectPrefixSink() {
61-
isSpringMvcViewResult(this.asExpr()) and
62-
appendedToRedirectPrefix(this)
59+
appendedToRedirectPrefix(this) and
60+
(
61+
isSpringMvcReturnedString(this.asExpr())
62+
or
63+
isSpringModelAndViewName(this.asExpr())
64+
)
65+
}
66+
}
67+
68+
/** A call to a helper that returns a Spring `"redirect:"` view name. */
69+
private class SpringUrlRedirectHelperSink extends UrlRedirectSink {
70+
SpringUrlRedirectHelperSink() {
71+
exists(MethodCall call |
72+
this.asExpr() = call and
73+
isSpringMvcViewResult(call) and
74+
returnsRedirectViewName(call.getCallee().getSourceDeclaration())
75+
)
6376
}
6477
}
6578

@@ -77,11 +90,15 @@ private class SpringRedirectPrefix extends InterestingPrefix {
7790
private predicate contributesToReturn(Expr value) {
7891
exists(ReturnStmt ret |
7992
ret.getEnclosingCallable() = value.getEnclosingCallable() and
80-
(
81-
value.getParent*() = ret.getExpr()
82-
or
83-
DataFlow::localFlow(DataFlow::exprNode(value), DataFlow::exprNode(ret.getExpr()))
84-
)
93+
DataFlow::localFlow(DataFlow::exprNode(value), DataFlow::exprNode(ret.getExpr()))
94+
)
95+
}
96+
97+
/** Holds if `value` contributes string content to the return value of its callable. */
98+
private predicate contributesToReturnedString(Expr value) {
99+
exists(ReturnStmt ret |
100+
ret.getEnclosingCallable() = value.getEnclosingCallable() and
101+
TaintTracking::localTaint(DataFlow::exprNode(value), DataFlow::exprNode(ret.getExpr()))
85102
)
86103
}
87104

@@ -91,25 +108,51 @@ private predicate isSpringMvcViewResult(Expr value) {
91108
contributesToReturn(value) and
92109
value.getEnclosingCallable() instanceof SpringRequestMappingMethod and
93110
not value.getEnclosingCallable().(SpringRequestMappingMethod).isResponseBody()
94-
or
95-
contributesToReturn(value) and
96-
exists(MethodCall call |
97-
call.getCallee().getSourceDeclaration() = value.getEnclosingCallable() and
98-
isSpringMvcViewResult(call)
111+
}
112+
113+
/** Holds if `value` contributes string content to a view name returned by a request handler. */
114+
private predicate isSpringMvcReturnedString(Expr value) {
115+
contributesToReturnedString(value) and
116+
value.getEnclosingCallable() instanceof SpringRequestMappingMethod and
117+
not value.getEnclosingCallable().(SpringRequestMappingMethod).isResponseBody()
118+
}
119+
120+
/** Holds if `value` contributes to the view name of a returned `ModelAndView`. */
121+
private predicate isSpringModelAndViewName(Expr value) {
122+
exists(ClassInstanceExpr newModelAndView |
123+
newModelAndView
124+
.getConstructedType()
125+
.hasQualifiedName("org.springframework.web.servlet", "ModelAndView") and
126+
TaintTracking::localTaint(DataFlow::exprNode(value),
127+
DataFlow::exprNode(newModelAndView.getArgument(0))) and
128+
isSpringMvcViewResult(newModelAndView)
99129
)
100130
}
101131

102-
private class SpringRedirectViewType extends RefType {
103-
SpringRedirectViewType() {
104-
this.getASupertype*().hasQualifiedName("org.springframework.web.servlet.view", "RedirectView")
105-
}
132+
/** Holds if `callable` returns a view name constructed with the Spring `"redirect:"` prefix. */
133+
pragma[nomagic]
134+
private predicate returnsRedirectViewName(Callable callable) {
135+
exists(DataFlow::ExprNode appended, ReturnStmt ret |
136+
appendedToRedirectPrefix(appended) and
137+
appended.asExpr().getEnclosingCallable() = callable and
138+
ret.getEnclosingCallable() = callable and
139+
TaintTracking::localTaint(appended, DataFlow::exprNode(ret.getExpr()))
140+
)
141+
or
142+
exists(MethodCall call |
143+
call.getEnclosingCallable() = callable and
144+
contributesToReturn(call) and
145+
returnsRedirectViewName(call.getCallee().getSourceDeclaration())
146+
)
106147
}
107148

108149
/** A URL passed to a Spring `RedirectView` constructor. */
109150
private class SpringRedirectViewSink extends UrlRedirectSink {
110151
SpringRedirectViewSink() {
111152
exists(ClassInstanceExpr newRedirectView |
112-
newRedirectView.getConstructedType() instanceof SpringRedirectViewType and
153+
newRedirectView
154+
.getConstructedType()
155+
.hasQualifiedName("org.springframework.web.servlet.view", "RedirectView") and
113156
isSpringMvcViewResult(newRedirectView) and
114157
this.asExpr() = newRedirectView.getArgument(0)
115158
)
@@ -120,14 +163,16 @@ private class SpringRedirectViewSink extends UrlRedirectSink {
120163
private class SpringRedirectViewSetUrlSink extends UrlRedirectSink {
121164
SpringRedirectViewSetUrlSink() {
122165
exists(MethodCall setUrl |
123-
setUrl.getMethod().hasName("setUrl") and
124-
setUrl.getMethod().getNumberOfParameters() = 1 and
125166
setUrl
126167
.getMethod()
127-
.getDeclaringType()
168+
.getSourceDeclaration()
169+
.hasQualifiedName("org.springframework.web.servlet.view", "AbstractUrlBasedView", "setUrl") and
170+
setUrl
171+
.getQualifier()
172+
.getType()
173+
.(RefType)
128174
.getASupertype*()
129-
.hasQualifiedName("org.springframework.web.servlet.view", "AbstractUrlBasedView") and
130-
setUrl.getQualifier().getType() instanceof SpringRedirectViewType and
175+
.hasQualifiedName("org.springframework.web.servlet.view", "RedirectView") and
131176
isSpringMvcViewResult(setUrl.getQualifier()) and
132177
this.asExpr() = setUrl.getArgument(0)
133178
)

java/ql/test/query-tests/security/CWE-601/semmle/tests/SpringUrlRedirect.java

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
class BaseController {
1212
protected String redirect(String path) {
13-
return "redirect:" + path; // $ Alert
13+
return "redirect:" + path;
1414
}
1515

1616
protected String ordinaryView(String path) {
@@ -22,10 +22,13 @@ protected String discardedRedirect(String path) {
2222
}
2323
}
2424

25-
class CustomRedirectView extends RedirectView {
26-
CustomRedirectView(String url) {
25+
class LabeledRedirectView extends RedirectView {
26+
LabeledRedirectView(String label, String url) {
2727
super(url);
2828
}
29+
30+
void setUrl(Object label) {
31+
}
2932
}
3033

3134
@Controller
@@ -51,7 +54,7 @@ public RedirectView redirectView(HttpServletRequest request) {
5154
@GetMapping("/case4")
5255
public String helperViewName(HttpServletRequest request) {
5356
String next = request.getHeader("referer"); // $ Source
54-
return redirect(next);
57+
return redirect(next); // $ Alert
5558
}
5659

5760
@GetMapping("/case5")
@@ -68,12 +71,6 @@ public RedirectView overloadedRedirectView(HttpServletRequest request) {
6871
return new RedirectView(next, true); // $ Alert
6972
}
7073

71-
@GetMapping("/case7")
72-
public RedirectView customRedirectView(HttpServletRequest request) {
73-
String next = request.getParameter("next"); // $ Source
74-
return new CustomRedirectView(next); // $ Alert
75-
}
76-
7774
@GetMapping("/safe-constant")
7875
public String constantViewName() {
7976
return "redirect:/account";
@@ -91,20 +88,36 @@ public ModelAndView ordinaryModelAndView(HttpServletRequest request) {
9188
return new ModelAndView(ordinaryView(view));
9289
}
9390

91+
@GetMapping("/safe-model-value")
92+
public ModelAndView redirectInModel(HttpServletRequest request) {
93+
String value = "redirect:" + request.getParameter("value");
94+
return new ModelAndView("home", "value", value);
95+
}
96+
9497
@GetMapping("/safe-discarded")
9598
public String discardedRedirectValue(HttpServletRequest request) {
9699
discardedRedirect(request.getParameter("next"));
97100
return "home";
98101
}
99102

100-
@GetMapping("/safe-validated")
101-
public RedirectView validatedRedirectView(HttpServletRequest request) {
102-
String next = request.getParameter("next");
103-
if ("https://example.com/account".equals(next)) {
104-
return new RedirectView("https://example.com/account");
105-
}
103+
@GetMapping("/safe-redirect-view")
104+
public RedirectView constantRedirectView() {
106105
return new RedirectView("https://example.com/account");
107106
}
107+
108+
@GetMapping("/safe-subclass-label")
109+
public RedirectView customRedirectView(HttpServletRequest request) {
110+
String label = request.getParameter("label");
111+
return new LabeledRedirectView(label, "/account");
112+
}
113+
114+
@GetMapping("/safe-set-url-overload")
115+
public RedirectView customSetUrl(HttpServletRequest request) {
116+
LabeledRedirectView view = new LabeledRedirectView("label", "/account");
117+
Object label = request.getParameter("label");
118+
view.setUrl(label);
119+
return view;
120+
}
108121
}
109122

110123
@Controller
@@ -123,3 +136,26 @@ public String responseBody(HttpServletRequest request) {
123136
return "redirect:" + request.getParameter("value");
124137
}
125138
}
139+
140+
class SharedRedirectHelper {
141+
protected String redirectShared(String path) {
142+
return "redirect:" + path;
143+
}
144+
}
145+
146+
@Controller
147+
class MvcViewUser extends SharedRedirectHelper {
148+
@GetMapping("/constant-view")
149+
public String view() {
150+
return redirectShared("/account");
151+
}
152+
}
153+
154+
@Controller
155+
class ResponseBodyUser extends SharedRedirectHelper {
156+
@ResponseBody
157+
@GetMapping("/body-user")
158+
public String body(HttpServletRequest request) {
159+
return redirectShared(request.getParameter("value"));
160+
}
161+
}

0 commit comments

Comments
 (0)