[Java] CWE-601 Spring url redirection detect#5844
Conversation
|
This could probably be a lot simpler and achieve similar results. The complication arises because you try to make sure e.g. a RedirectView is returned from a request handler. However, is there any other reason to create a RedirectView instance? We could simply make the argument to RedirectView.setUrl a sink. Looking at your example .java file, it looks like you care about:
How about simply considering those three (really, two) cases sinks and assuming that in 99% of cases the resulting object or string will get returned from a request handler? Then if FPs arise because actually the RedirectView was made for a different reason we can bring in the higher complexity seen in this version. Regarding the |
| ma.getMethod().hasName("setUrl") and | ||
| ma.getMethod() | ||
| .getDeclaringType() | ||
| .hasQualifiedName("org.springframework.web.servlet.view", "AbstractUrlBasedView") and |
There was a problem hiding this comment.
I'm very sorry, I should make changes here. There are many subclasses of AbstractUrlBasedView. For now, I only found that the subclasses of RedirectView can cause url redirection.
For There are several ways to trigger URL redirection in Spring:
|
|
Unless tracking to the return site adds value, then you should simplify the query and only track as far as the |
If setUrl is used as a sink, there should still be a data flow return from the setUrl to Spring controller method. Because just setUrl won't trigger a vulnerability. |
|
Is there any conceivable reason someone would call |
Okay, then do I need to put it in the experimental directory or under the CWE-601 in the secure directory? |
|
@smowton Please review again. |
| or | ||
| exists(ClassInstanceExpr cie | | ||
| cie.getConstructedType().hasQualifiedName("org.springframework.web.servlet", "ModelAndView") and | ||
| cie.getArgument(0) = this.asExpr() and | ||
| exists(RedirectBuilderFlowConfig rstrbfc | rstrbfc.hasFlowToExpr(cie.getArgument(0))) | ||
| ) |
There was a problem hiding this comment.
| or | |
| exists(ClassInstanceExpr cie | | |
| cie.getConstructedType().hasQualifiedName("org.springframework.web.servlet", "ModelAndView") and | |
| cie.getArgument(0) = this.asExpr() and | |
| exists(RedirectBuilderFlowConfig rstrbfc | rstrbfc.hasFlowToExpr(cie.getArgument(0))) | |
| ) |
Now subsumed by the case exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = this.asExpr())
| /** A data flow configuration tracing flow from remote sources to redirect builder expression. */ | ||
| private class RedirectBuilderFlowConfig extends DataFlow2::Configuration { | ||
| RedirectBuilderFlowConfig() { this = "RedirectBuilderFlowConfig" } | ||
|
|
||
| override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } | ||
|
|
||
| override predicate isSink(DataFlow::Node sink) { | ||
| exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = sink.asExpr()) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
| /** A data flow configuration tracing flow from remote sources to redirect builder expression. */ | |
| private class RedirectBuilderFlowConfig extends DataFlow2::Configuration { | |
| RedirectBuilderFlowConfig() { this = "RedirectBuilderFlowConfig" } | |
| override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } | |
| override predicate isSink(DataFlow::Node sink) { | |
| exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = sink.asExpr()) | |
| } | |
| } |
No longer needed
| or | ||
| exists(MethodAccess ma | | ||
| ma.getMethod().hasName("setUrl") and | ||
| ma.getMethod() |
There was a problem hiding this comment.
When evaluating this query I will try a version that replaces this with simply detecting RedirectView.setUrl and see if there are any extra results.
There was a problem hiding this comment.
No problem, I'm just worried about false negatives.
|
Are you intending a https://github.com/github/securitylab-bounties/issues issue for this? |
Yes. After you review there is no problem. |
| // E.g: `String url = "/path?token=" + request.getParameter("token");` | ||
| exists(AddExpr ae | | ||
| ae.getRightOperand() = node.asExpr() and | ||
| not ae instanceof RedirectBuilderExpr |
There was a problem hiding this comment.
I think other concatenations such as StringBuffer ans StringBuilder should be accounted for.
There was a problem hiding this comment.
it's great. I will add and verify immediately.
There was a problem hiding this comment.
It would be nice to also account for prefixes in format strings such as in https://lgtm.com/projects/g/Yuri-Krukovski/my-sample/snapshot/767b6c59de570cfbfeeaf81188488bd8d2958b56/files/ngo-admin/src/main/java/ngo/skarb/ui/controller/story/SuccessStoryStatusChangeController.java?sort=name&dir=ASC&mode=heatmap#L76
So that if the tainted var is not in the first argument position or if the string format does not start with a placeholder, we can consider it as a prefixed string
| */ | ||
| class RedirectBuilderExpr extends AddExpr { | ||
| RedirectBuilderExpr() { | ||
| this.getLeftOperand().(CompileTimeConstantExpr).getStringValue() = "redirect:" |
There was a problem hiding this comment.
IIRC there are other interesting prefixes you could account for here:ajaxredirect: and forward: although the second one may lead to file disclosure rather than open redirect
https://grep.app/search?q=%22forward%3A%22
https://grep.app/search?q=%22ajaxredirect%3A%22
| /** A URL redirection sink from spring controller method. */ | ||
| class SpringUrlRedirectSink extends DataFlow::Node { | ||
| SpringUrlRedirectSink() { | ||
| exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = this.asExpr()) |
There was a problem hiding this comment.
Shouldnt we make sure the "redirect:" prefixed string flows into the return expression of a controller method?
There was a problem hiding this comment.
I think @smowton refered to the RedirectView and setUrl cases where these objects are used only for constructing Views so they will probably be used. But just concatenating "redirect:" is different since its very generic, you may even find it on non-Spring applications
There was a problem hiding this comment.
Ah damn you're right, I did suggest redirect: + <sink> as itself forming a sink, to avoid the usual pitfalls of daisy-chaining two global dataflow configs together. How about using that as a sink + ensuring we're looking at a Spring application by checking a relevant Spring type exists in the database? In such a context would you consider "redirect:" + <sink> sufficiently suspicious?
There was a problem hiding this comment.
@smowton How about checking that the enclosing callable is reached from a Spring controller method, something along the lines of
exists(Method controller_method |
controller_method.getAnAnnotation().getType().getName() = ["RequestMapping", "GetMapping", "PostMapping", ...] and
controller_method.getACallee*() = sink.getEnclosingCallable()
)There was a problem hiding this comment.
exists(Method controller_method |
controller_method.getAnAnnotation().getType().getName() = ["RequestMapping", "GetMapping", "PostMapping"] and
any(controller_method).polyCalls*(sink.getEnclosingCallable())
)@pwntester This should be what you want.
There was a problem hiding this comment.
That any isn't doing anything.
How about
any(Method controller_method | controller_method.getAnAnnotation().getType().getName() = ["RequestMapping", "GetMapping", "PostMapping"]).polyCalls*(sink.getEnclosingCallable())
There was a problem hiding this comment.
Cool, just learnt about polyCalls is it semantically similar to what I proposed?
There was a problem hiding this comment.
Yes but it traverses virtual dispatch too (e.g. Object.toString perhaps actually calling String.toString)
Please use the right label ( |
Thanks for the reminder, I took a look, the label is not wrong. |
I meant that you open this PR 7 days ago which got the CodeQL team engaged, but did not open the bounty submission issue till 1 hour ago so the SecLab was not pulled in from the beggining. |
Understood, I'm sorry about that. |
|
Screw it, enough guessing, I've started three evaluations of this, one as-is, one dropping the RedirectView auxiliary flow tracking, and one dropping both auxiliary flow configs. |
|
Results from the evaluation: drop the RedirectViewFlowConfig side condition; replace the RedirectBuilderFlowConfig side condition with the |
I think it should be like this? any(SpringRequestMappingMethod sqmm).polyCalls*(sink.getEnclosingCallable()) |
👍 |
I made the changes again, please review again, thank you. |
| import semmle.code.java.dataflow.TaintTracking | ||
| import semmle.code.java.frameworks.spring.SpringController | ||
|
|
||
| class StartsWithSanitizer extends DataFlow::BarrierGuard { |
| override predicate isSanitizer(DataFlow::Node node) { | ||
| // Exclude the case where the left side of the concatenated string is not `redirect:`. | ||
| // E.g: `String url = "/path?token=" + request.getParameter("token");` | ||
| exists(AddExpr ae | |
There was a problem hiding this comment.
| exists(AddExpr ae | | |
| // Note this is quite a broad sanitizer (it will also sanitize the right-hand side of `url = "http://" + request.getParameter("token")`); | |
| // Consider making this stricter in future. | |
| exists(AddExpr ae | |
|
|
Damn, I know why. |
Please review again. |
|
@smowton @pwntester thanks. |
The Sping framework implements the redirection function, and the commonly used methods are:
When the program merges the user input directly into the URL redirect request without verification, can carry out phishing attacks on users.