Skip to content

Commit d889e3c

Browse files
authored
Merge pull request #14854 from jcogs33/jcogs33/unsafe-url-forward-promotion
Java: Promote Unsafe URL Forward query from experimental
2 parents 5b1cae5 + 2f8c4df commit d889e3c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+758
-1307
lines changed

java/ql/lib/ext/experimental/io.undertow.server.handlers.resource.model.yml

Lines changed: 0 additions & 8 deletions
This file was deleted.

java/ql/lib/ext/experimental/java.nio.file.model.yml

Lines changed: 0 additions & 10 deletions
This file was deleted.

java/ql/lib/ext/experimental/java.util.concurrent.model.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,3 @@ extensions:
44
extensible: experimentalSinkModel
55
data:
66
- ["java.util.concurrent", "TimeUnit", True, "sleep", "", "", "Argument[0]", "thread-pause", "manual", "thread-resource-abuse"]
7-
- ["java.util.concurrent", "TimeUnit", True, "sleep", "", "", "Argument[0]", "thread-pause", "manual", "unsafe-url-forward"]
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
extensions:
2-
- addsTo:
3-
pack: codeql/java-all
4-
extensible: experimentalSourceModel
5-
data:
6-
- ["javax.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual", "unsafe-url-forward"]
72
- addsTo:
83
pack: codeql/java-all
94
extensible: experimentalSourceModel
@@ -13,4 +8,3 @@ extensions:
138
- ["javax.servlet.http", "HttpServletRequest", False, "getRequestURI", "()", "", "ReturnValue", "uri-path", "manual", "permissive-dot-regex-query"]
149
- ["javax.servlet.http", "HttpServletRequest", False, "getRequestURL", "()", "", "ReturnValue", "uri-path", "manual", "permissive-dot-regex-query"]
1510
- ["javax.servlet.http", "HttpServletRequest", False, "getServletPath", "()", "", "ReturnValue", "uri-path", "manual", "permissive-dot-regex-query"]
16-

java/ql/lib/ext/experimental/org.springframework.core.io.model.yml

Lines changed: 0 additions & 16 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
extensions:
22
- addsTo:
33
pack: codeql/java-all
4-
extensible: experimentalSourceModel
4+
extensible: sourceModel
55
data:
6-
- ["jakarta.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual", "unsafe-url-forward"]
6+
- ["jakarta.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual"]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/java-all
4+
extensible: sinkModel
5+
data:
6+
- ["jakarta.servlet", "ServletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
7+
- ["jakarta.servlet", "ServletRequest", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/java-all
4+
extensible: sinkModel
5+
data:
6+
- ["javax.portlet", "PortletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]

java/ql/lib/ext/javax.servlet.http.model.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ extensions:
1818
- ["javax.servlet.http", "HttpServletRequest", False, "getRemoteUser", "()", "", "ReturnValue", "remote", "manual"]
1919
- ["javax.servlet.http", "HttpServletRequest", False, "getRequestURI", "()", "", "ReturnValue", "remote", "manual"]
2020
- ["javax.servlet.http", "HttpServletRequest", False, "getRequestURL", "()", "", "ReturnValue", "remote", "manual"]
21+
- ["javax.servlet.http", "HttpServletRequest", False, "getServletPath", "()", "", "ReturnValue", "remote", "manual"]
22+
2123
- addsTo:
2224
pack: codeql/java-all
2325
extensible: sinkModel

java/ql/lib/ext/javax.servlet.model.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ extensions:
1414
extensible: sinkModel
1515
data:
1616
- ["javax.servlet", "ServletContext", True, "getResourceAsStream", "(String)", "", "Argument[0]", "path-injection", "ai-manual"]
17+
- ["javax.servlet", "ServletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
18+
- ["javax.servlet", "ServletRequest", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
1719
- addsTo:
1820
pack: codeql/java-all
1921
extensible: summaryModel

java/ql/lib/ext/org.kohsuke.stapler.model.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ extensions:
99
- ["org.kohsuke.stapler", "HttpResponses", True, "staticResource", "(URL,long)", "", "Argument[0]", "request-forgery", "manual"]
1010
- ["org.kohsuke.stapler", "HttpResponses", True, "html", "(String)", "", "Argument[0]", "html-injection", "manual"]
1111
- ["org.kohsuke.stapler", "HttpResponses", True, "literalHtml", "(String)", "", "Argument[0]", "html-injection", "manual"]
12-
- ["org.kohsuke.stapler", "StaplerResponse", True, "forward", "(Object,String,StaplerRequest)", "", "Argument[1]", "request-forgery", "manual"]
12+
- ["org.kohsuke.stapler", "StaplerResponse", True, "forward", "(Object,String,StaplerRequest)", "", "Argument[1]", "url-forward", "manual"]
1313
- ["org.kohsuke.stapler", "StaplerResponse", True, "sendRedirect2", "(String)", "", "Argument[0]", "url-redirection", "manual"]
1414
- ["org.kohsuke.stapler", "StaplerResponse", True, "sendRedirect", "(int,String)", "", "Argument[1]", "url-redirection", "manual"]
1515
- ["org.kohsuke.stapler", "StaplerResponse", True, "sendRedirect", "(String)", "", "Argument[0]", "url-redirection", "manual"]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/java-all
4+
extensible: sinkModel
5+
data:
6+
- ["org.springframework.web.portlet", "ModelAndView", False, "ModelAndView", "", "", "Argument[0]", "url-forward", "manual"]
7+
- ["org.springframework.web.portlet", "ModelAndView", False, "setViewName", "", "", "Argument[0]", "url-forward", "manual"]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/java-all
4+
extensible: sinkModel
5+
data:
6+
- ["org.springframework.web.servlet", "ModelAndView", False, "ModelAndView", "", "", "Argument[0]", "url-forward", "manual"]
7+
- ["org.springframework.web.servlet", "ModelAndView", False, "setViewName", "", "", "Argument[0]", "url-forward", "manual"]

java/ql/lib/semmle/code/java/JDK.qll

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ class StringLengthMethod extends Method {
3838
StringLengthMethod() { this.hasName("length") and this.getDeclaringType() instanceof TypeString }
3939
}
4040

41+
/** The `contains()` method of the class `java.lang.String`. */
42+
class StringContainsMethod extends Method {
43+
StringContainsMethod() {
44+
this.hasName("contains") and this.getDeclaringType() instanceof TypeString
45+
}
46+
}
47+
4148
/**
4249
* The methods on the class `java.lang.String` that are used to perform partial matches with a specified substring or char.
4350
*/

java/ql/lib/semmle/code/java/frameworks/Networking.qll

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ class TypeUrl extends RefType {
2424
TypeUrl() { this.hasQualifiedName("java.net", "URL") }
2525
}
2626

27+
/** The type `java.net.URLDecoder`. */
28+
class TypeUrlDecoder extends RefType {
29+
TypeUrlDecoder() { this.hasQualifiedName("java.net", "URLDecoder") }
30+
}
31+
2732
/** The type `java.net.URI`. */
2833
class TypeUri extends RefType {
2934
TypeUri() { this.hasQualifiedName("java.net", "URI") }
@@ -157,6 +162,14 @@ class UrlOpenConnectionMethod extends Method {
157162
}
158163
}
159164

165+
/** The method `java.net.URLDecoder::decode`. */
166+
class UrlDecodeMethod extends Method {
167+
UrlDecodeMethod() {
168+
this.getDeclaringType() instanceof TypeUrlDecoder and
169+
this.getName() = "decode"
170+
}
171+
}
172+
160173
/** The method `javax.net.SocketFactory::createSocket`. */
161174
class CreateSocketMethod extends Method {
162175
CreateSocketMethod() {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ private predicate exactPathMatchGuard(Guard g, Expr e, boolean branch) {
6464
)
6565
}
6666

67-
private class ExactPathMatchSanitizer extends PathInjectionSanitizer {
67+
/**
68+
* A sanitizer that protects against path injection vulnerabilities
69+
* by checking for a matching path.
70+
*/
71+
class ExactPathMatchSanitizer extends PathInjectionSanitizer {
6872
ExactPathMatchSanitizer() {
6973
this = DataFlow::BarrierGuard<exactPathMatchGuard/3>::getABarrierNode()
7074
or
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/** Provides classes and a taint-tracking configuration to reason about unsafe URL forwarding. */
2+
3+
import java
4+
private import semmle.code.java.dataflow.ExternalFlow
5+
private import semmle.code.java.dataflow.FlowSources
6+
private import semmle.code.java.dataflow.StringPrefixes
7+
private import semmle.code.java.security.PathSanitizer
8+
private import semmle.code.java.controlflow.Guards
9+
private import semmle.code.java.security.Sanitizers
10+
11+
/** A URL forward sink. */
12+
abstract class UrlForwardSink extends DataFlow::Node { }
13+
14+
/**
15+
* A default sink representing methods susceptible to URL
16+
* forwarding attacks.
17+
*/
18+
private class DefaultUrlForwardSink extends UrlForwardSink {
19+
DefaultUrlForwardSink() { sinkNode(this, "url-forward") }
20+
}
21+
22+
/**
23+
* An expression appended (perhaps indirectly) to `"forward:"`
24+
* and reachable from a Spring entry point.
25+
*/
26+
private class SpringUrlForwardPrefixSink extends UrlForwardSink {
27+
SpringUrlForwardPrefixSink() {
28+
any(SpringRequestMappingMethod srmm).polyCalls*(this.getEnclosingCallable()) and
29+
appendedToForwardPrefix(this)
30+
}
31+
}
32+
33+
pragma[nomagic]
34+
private predicate appendedToForwardPrefix(DataFlow::ExprNode exprNode) {
35+
exists(ForwardPrefix fp | exprNode.asExpr() = fp.getAnAppendedExpression())
36+
}
37+
38+
private class ForwardPrefix extends InterestingPrefix {
39+
ForwardPrefix() { this.getStringValue() = "forward:" }
40+
41+
override int getOffset() { result = 0 }
42+
}
43+
44+
/** A URL forward barrier. */
45+
abstract class UrlForwardBarrier extends DataFlow::Node { }
46+
47+
private class PrimitiveBarrier extends UrlForwardBarrier instanceof SimpleTypeSanitizer { }
48+
49+
/**
50+
* A barrier for values appended to a "redirect:" prefix.
51+
* These results are excluded because they should be handled
52+
* by the `java/unvalidated-url-redirection` query instead.
53+
*/
54+
private class RedirectPrefixBarrier extends UrlForwardBarrier {
55+
RedirectPrefixBarrier() { this.asExpr() = any(RedirectPrefix fp).getAnAppendedExpression() }
56+
}
57+
58+
private class RedirectPrefix extends InterestingPrefix {
59+
RedirectPrefix() { this.getStringValue() = "redirect:" }
60+
61+
override int getOffset() { result = 0 }
62+
}
63+
64+
/**
65+
* A value that is the result of prepending a string that prevents
66+
* any value from controlling the path of a URL.
67+
*/
68+
private class FollowsBarrierPrefix extends UrlForwardBarrier {
69+
FollowsBarrierPrefix() { this.asExpr() = any(BarrierPrefix fp).getAnAppendedExpression() }
70+
}
71+
72+
private class BarrierPrefix extends InterestingPrefix {
73+
int offset;
74+
75+
BarrierPrefix() {
76+
// Matches strings that look like when prepended to untrusted input, they will restrict
77+
// the path of a URL: for example, anything containing `?` or `#`.
78+
exists(this.getStringValue().regexpFind("[?#]", 0, offset))
79+
or
80+
this.(CharacterLiteral).getValue() = ["?", "#"] and offset = 0
81+
}
82+
83+
override int getOffset() { result = offset }
84+
}
85+
86+
/**
87+
* A barrier that protects against path injection vulnerabilities
88+
* while accounting for URL encoding.
89+
*/
90+
private class UrlPathBarrier extends UrlForwardBarrier instanceof PathInjectionSanitizer {
91+
UrlPathBarrier() {
92+
this instanceof ExactPathMatchSanitizer or
93+
this instanceof NoUrlEncodingBarrier or
94+
this instanceof FullyDecodesUrlBarrier
95+
}
96+
}
97+
98+
/** A call to a method that decodes a URL. */
99+
abstract class UrlDecodeCall extends MethodCall { }
100+
101+
private class DefaultUrlDecodeCall extends UrlDecodeCall {
102+
DefaultUrlDecodeCall() {
103+
this.getMethod() instanceof UrlDecodeMethod or
104+
this.getMethod().hasQualifiedName("org.eclipse.jetty.util.URIUtil", "URIUtil", "decodePath")
105+
}
106+
}
107+
108+
/** A repeated call to a method that decodes a URL. */
109+
abstract class RepeatedUrlDecodeCall extends MethodCall { }
110+
111+
private class DefaultRepeatedUrlDecodeCall extends RepeatedUrlDecodeCall instanceof UrlDecodeCall {
112+
DefaultRepeatedUrlDecodeCall() { this.getAnEnclosingStmt() instanceof LoopStmt }
113+
}
114+
115+
/** A method call that checks a string for URL encoding. */
116+
abstract class CheckUrlEncodingCall extends MethodCall { }
117+
118+
private class DefaultCheckUrlEncodingCall extends CheckUrlEncodingCall {
119+
DefaultCheckUrlEncodingCall() {
120+
this.getMethod() instanceof StringContainsMethod and
121+
this.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "%"
122+
}
123+
}
124+
125+
/** A guard that looks for a method call that checks for URL encoding. */
126+
private class CheckUrlEncodingGuard extends Guard instanceof CheckUrlEncodingCall {
127+
Expr getCheckedExpr() { result = this.(MethodCall).getQualifier() }
128+
}
129+
130+
/** Holds if `g` is guard for a URL that does not contain URL encoding. */
131+
private predicate noUrlEncodingGuard(Guard g, Expr e, boolean branch) {
132+
e = g.(CheckUrlEncodingGuard).getCheckedExpr() and
133+
branch = false
134+
or
135+
branch = false and
136+
g.(Expr).getType() instanceof BooleanType and
137+
(
138+
exists(CheckUrlEncodingCall call, AssignExpr ae |
139+
ae.getSource() = call and
140+
e = call.getQualifier() and
141+
g = ae.getDest()
142+
)
143+
or
144+
exists(CheckUrlEncodingCall call, LocalVariableDeclExpr vde |
145+
vde.getInitOrPatternSource() = call and
146+
e = call.getQualifier() and
147+
g = vde.getAnAccess()
148+
)
149+
)
150+
}
151+
152+
/** A barrier for URLs that do not contain URL encoding. */
153+
private class NoUrlEncodingBarrier extends DataFlow::Node {
154+
NoUrlEncodingBarrier() { this = DataFlow::BarrierGuard<noUrlEncodingGuard/3>::getABarrierNode() }
155+
}
156+
157+
/** Holds if `g` is guard for a URL that is fully decoded. */
158+
private predicate fullyDecodesUrlGuard(Expr e) {
159+
exists(CheckUrlEncodingGuard g, RepeatedUrlDecodeCall decodeCall |
160+
e = g.getCheckedExpr() and
161+
g.controls(decodeCall.getBasicBlock(), true)
162+
)
163+
}
164+
165+
/** A barrier for URLs that are fully decoded. */
166+
private class FullyDecodesUrlBarrier extends DataFlow::Node {
167+
FullyDecodesUrlBarrier() {
168+
exists(Variable v, Expr e | this.asExpr() = v.getAnAccess() |
169+
fullyDecodesUrlGuard(e) and
170+
e = v.getAnAccess() and
171+
e.getBasicBlock().bbDominates(this.asExpr().getBasicBlock())
172+
)
173+
}
174+
}
175+
176+
/**
177+
* A taint-tracking configuration for reasoning about URL forwarding.
178+
*/
179+
module UrlForwardFlowConfig implements DataFlow::ConfigSig {
180+
predicate isSource(DataFlow::Node source) {
181+
source instanceof ThreatModelFlowSource and
182+
// excluded due to FPs
183+
not exists(MethodCall mc, Method m |
184+
m instanceof HttpServletRequestGetRequestUriMethod or
185+
m instanceof HttpServletRequestGetRequestUrlMethod or
186+
m instanceof HttpServletRequestGetPathMethod
187+
|
188+
mc.getMethod() = m and
189+
mc = source.asExpr()
190+
)
191+
}
192+
193+
predicate isSink(DataFlow::Node sink) { sink instanceof UrlForwardSink }
194+
195+
predicate isBarrier(DataFlow::Node node) { node instanceof UrlForwardBarrier }
196+
197+
DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
198+
}
199+
200+
/**
201+
* Taint-tracking flow for URL forwarding.
202+
*/
203+
module UrlForwardFlow = TaintTracking::Global<UrlForwardFlowConfig>;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
public class UrlForward extends HttpServlet {
2+
private static final String VALID_FORWARD = "https://cwe.mitre.org/data/definitions/552.html";
3+
4+
protected void doGet(HttpServletRequest request, HttpServletResponse response)
5+
throws ServletException, IOException {
6+
ServletConfig cfg = getServletConfig();
7+
ServletContext sc = cfg.getServletContext();
8+
9+
// BAD: a request parameter is incorporated without validation into a URL forward
10+
sc.getRequestDispatcher(request.getParameter("target")).forward(request, response);
11+
12+
// GOOD: the request parameter is validated against a known fixed string
13+
if (VALID_FORWARD.equals(request.getParameter("target"))) {
14+
sc.getRequestDispatcher(VALID_FORWARD).forward(request, response);
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)