Skip to content

Java: Promote Unsafe URL Forward query from experimental #14854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0d38a96
Java: copy files from experimental
Nov 20, 2023
2793f28
Java: move config to Query.qll file
Nov 20, 2023
35a083a
Java: update test cases to use inline expectations
Nov 20, 2023
915e106
Java: remove path-injection related models and tests for now
Nov 20, 2023
2a68299
Java: move MaD models to correct files, delete ones that already exist
Nov 30, 2023
4ff884e
Java: remove more path-injection related classes (will maybe add some…
Nov 30, 2023
42e3825
Java: convert RequestDispatcherSink to MaD
Mar 5, 2024
8d66097
Java: switch StaplerResponse.forward from request-forgery sink to url…
Nov 30, 2023
1da1e89
Java: convert SpringModelAndViewSink to MaD
Mar 5, 2024
5a9d755
Java: add some comments and minor code reorg
Nov 30, 2023
6e7c054
Java: update query metadata and alert message
Dec 1, 2023
09bc21d
Java: rename 'UnsafeUrlForward' to 'UrlForward'
Dec 1, 2023
c331393
Java: update qhelp
Dec 1, 2023
5fa63ab
Java: update/add some TODO comments
Dec 4, 2023
e75c96c
Java: combine test cases; add test for StaplerResponse.forward
Dec 11, 2023
c8ec301
Java: add change note
Dec 11, 2023
911a61d
Java: initial update of barrier and test cases to remove FN
Mar 5, 2024
f573032
Java: remove todo comments from ext files
Mar 5, 2024
2708e53
Java: remove redundant imports
Mar 5, 2024
43b4962
Java: use new 'SimpleTypeSanitizer', and update some non-extending su…
Mar 5, 2024
d9772c1
Java: update change note
Mar 5, 2024
d220b3a
Java: some updates to test cases
Mar 10, 2024
052452b
Java: create UrlDecodeMethod
Mar 10, 2024
042dcf9
Java: some updates to UrlPathBarrier code
Mar 10, 2024
a807596
Java: add QLDocs to UrlPathBarrier code
Mar 10, 2024
a002674
Java: clean up comments on test cases
Mar 11, 2024
7310c15
Java: rename SpringUrlForwardSink
Mar 11, 2024
c5a59d6
Java: add QLDoc
Mar 11, 2024
e99cea3
Java: update UrlPathBarrier to include FollowsBarrierPrefix
Mar 12, 2024
04d27f2
Java: adjust prefix barriers
Mar 13, 2024
5ac453e
Java: add spurious test case for StringBuilder.append
Mar 13, 2024
1b01f26
Java: adjust BarrierPrefix to handle prepended chars
Mar 13, 2024
55f7369
Java: performance fix
Mar 15, 2024
658fffe
Java: remove experimental files
Mar 18, 2024
a8eb1d1
Java: remove experimental tests
Mar 18, 2024
35fbc95
Java: remove redundant line
Mar 27, 2024
121b24e
Java: remove parentheses
Mar 27, 2024
2391fe7
Java: use InlineFlowTest instead of InlineExpectationsTest
Mar 27, 2024
40c932a
Java: move UrlForward.qll code to UrlForwardQuery.qll
Mar 27, 2024
2f8c4df
docs wording updates
jcogs33 Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

10 changes: 0 additions & 10 deletions java/ql/lib/ext/experimental/java.nio.file.model.yml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ extensions:
extensible: experimentalSinkModel
data:
- ["java.util.concurrent", "TimeUnit", True, "sleep", "", "", "Argument[0]", "thread-pause", "manual", "thread-resource-abuse"]
- ["java.util.concurrent", "TimeUnit", True, "sleep", "", "", "Argument[0]", "thread-pause", "manual", "unsafe-url-forward"]
6 changes: 0 additions & 6 deletions java/ql/lib/ext/experimental/javax.servlet.http.model.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
extensions:
- addsTo:
pack: codeql/java-all
extensible: experimentalSourceModel
data:
- ["javax.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual", "unsafe-url-forward"]
- addsTo:
pack: codeql/java-all
extensible: experimentalSourceModel
Expand All @@ -13,4 +8,3 @@ extensions:
- ["javax.servlet.http", "HttpServletRequest", False, "getRequestURI", "()", "", "ReturnValue", "uri-path", "manual", "permissive-dot-regex-query"]
- ["javax.servlet.http", "HttpServletRequest", False, "getRequestURL", "()", "", "ReturnValue", "uri-path", "manual", "permissive-dot-regex-query"]
- ["javax.servlet.http", "HttpServletRequest", False, "getServletPath", "()", "", "ReturnValue", "uri-path", "manual", "permissive-dot-regex-query"]

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
extensions:
- addsTo:
pack: codeql/java-all
extensible: experimentalSourceModel
extensible: sourceModel
data:
- ["jakarta.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual", "unsafe-url-forward"]
- ["jakarta.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual"]
7 changes: 7 additions & 0 deletions java/ql/lib/ext/jakarta.servlet.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extensions:
- addsTo:
pack: codeql/java-all
extensible: sinkModel
data:
- ["jakarta.servlet", "ServletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
- ["jakarta.servlet", "ServletRequest", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
6 changes: 6 additions & 0 deletions java/ql/lib/ext/javax.portlet.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
extensions:
- addsTo:
pack: codeql/java-all
extensible: sinkModel
data:
- ["javax.portlet", "PortletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
2 changes: 2 additions & 0 deletions java/ql/lib/ext/javax.servlet.http.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ extensions:
- ["javax.servlet.http", "HttpServletRequest", False, "getRemoteUser", "()", "", "ReturnValue", "remote", "manual"]
- ["javax.servlet.http", "HttpServletRequest", False, "getRequestURI", "()", "", "ReturnValue", "remote", "manual"]
- ["javax.servlet.http", "HttpServletRequest", False, "getRequestURL", "()", "", "ReturnValue", "remote", "manual"]
- ["javax.servlet.http", "HttpServletRequest", False, "getServletPath", "()", "", "ReturnValue", "remote", "manual"]

- addsTo:
pack: codeql/java-all
extensible: sinkModel
Expand Down
2 changes: 2 additions & 0 deletions java/ql/lib/ext/javax.servlet.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ extensions:
extensible: sinkModel
data:
- ["javax.servlet", "ServletContext", True, "getResourceAsStream", "(String)", "", "Argument[0]", "path-injection", "ai-manual"]
- ["javax.servlet", "ServletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
- ["javax.servlet", "ServletRequest", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
- addsTo:
pack: codeql/java-all
extensible: summaryModel
Expand Down
2 changes: 1 addition & 1 deletion java/ql/lib/ext/org.kohsuke.stapler.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ extensions:
- ["org.kohsuke.stapler", "HttpResponses", True, "staticResource", "(URL,long)", "", "Argument[0]", "request-forgery", "manual"]
- ["org.kohsuke.stapler", "HttpResponses", True, "html", "(String)", "", "Argument[0]", "html-injection", "manual"]
- ["org.kohsuke.stapler", "HttpResponses", True, "literalHtml", "(String)", "", "Argument[0]", "html-injection", "manual"]
- ["org.kohsuke.stapler", "StaplerResponse", True, "forward", "(Object,String,StaplerRequest)", "", "Argument[1]", "request-forgery", "manual"]
- ["org.kohsuke.stapler", "StaplerResponse", True, "forward", "(Object,String,StaplerRequest)", "", "Argument[1]", "url-forward", "manual"]
- ["org.kohsuke.stapler", "StaplerResponse", True, "sendRedirect2", "(String)", "", "Argument[0]", "url-redirection", "manual"]
- ["org.kohsuke.stapler", "StaplerResponse", True, "sendRedirect", "(int,String)", "", "Argument[1]", "url-redirection", "manual"]
- ["org.kohsuke.stapler", "StaplerResponse", True, "sendRedirect", "(String)", "", "Argument[0]", "url-redirection", "manual"]
Expand Down
7 changes: 7 additions & 0 deletions java/ql/lib/ext/org.springframework.web.portlet.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extensions:
- addsTo:
pack: codeql/java-all
extensible: sinkModel
data:
- ["org.springframework.web.portlet", "ModelAndView", False, "ModelAndView", "", "", "Argument[0]", "url-forward", "manual"]
- ["org.springframework.web.portlet", "ModelAndView", False, "setViewName", "", "", "Argument[0]", "url-forward", "manual"]
7 changes: 7 additions & 0 deletions java/ql/lib/ext/org.springframework.web.servlet.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extensions:
- addsTo:
pack: codeql/java-all
extensible: sinkModel
data:
- ["org.springframework.web.servlet", "ModelAndView", False, "ModelAndView", "", "", "Argument[0]", "url-forward", "manual"]
- ["org.springframework.web.servlet", "ModelAndView", False, "setViewName", "", "", "Argument[0]", "url-forward", "manual"]
7 changes: 7 additions & 0 deletions java/ql/lib/semmle/code/java/JDK.qll
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ class StringLengthMethod extends Method {
StringLengthMethod() { this.hasName("length") and this.getDeclaringType() instanceof TypeString }
}

/** The `contains()` method of the class `java.lang.String`. */
class StringContainsMethod extends Method {
StringContainsMethod() {
this.hasName("contains") and this.getDeclaringType() instanceof TypeString
}
}

/**
* The methods on the class `java.lang.String` that are used to perform partial matches with a specified substring or char.
*/
Expand Down
13 changes: 13 additions & 0 deletions java/ql/lib/semmle/code/java/frameworks/Networking.qll
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ class TypeUrl extends RefType {
TypeUrl() { this.hasQualifiedName("java.net", "URL") }
}

/** The type `java.net.URLDecoder`. */
class TypeUrlDecoder extends RefType {
TypeUrlDecoder() { this.hasQualifiedName("java.net", "URLDecoder") }
}

/** The type `java.net.URI`. */
class TypeUri extends RefType {
TypeUri() { this.hasQualifiedName("java.net", "URI") }
Expand Down Expand Up @@ -157,6 +162,14 @@ class UrlOpenConnectionMethod extends Method {
}
}

/** The method `java.net.URLDecoder::decode`. */
class UrlDecodeMethod extends Method {
UrlDecodeMethod() {
this.getDeclaringType() instanceof TypeUrlDecoder and
this.getName() = "decode"
}
}

/** The method `javax.net.SocketFactory::createSocket`. */
class CreateSocketMethod extends Method {
CreateSocketMethod() {
Expand Down
6 changes: 5 additions & 1 deletion java/ql/lib/semmle/code/java/security/PathSanitizer.qll
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ private predicate exactPathMatchGuard(Guard g, Expr e, boolean branch) {
)
}

private class ExactPathMatchSanitizer extends PathInjectionSanitizer {
/**
* A sanitizer that protects against path injection vulnerabilities
* by checking for a matching path.
*/
class ExactPathMatchSanitizer extends PathInjectionSanitizer {
ExactPathMatchSanitizer() {
this = DataFlow::BarrierGuard<exactPathMatchGuard/3>::getABarrierNode()
or
Expand Down
203 changes: 203 additions & 0 deletions java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/** Provides classes and a taint-tracking configuration to reason about unsafe URL forwarding. */

import java
private import semmle.code.java.dataflow.ExternalFlow
private import semmle.code.java.dataflow.FlowSources
private import semmle.code.java.dataflow.StringPrefixes
private import semmle.code.java.security.PathSanitizer
private import semmle.code.java.controlflow.Guards
private import semmle.code.java.security.Sanitizers

/** A URL forward sink. */
abstract class UrlForwardSink extends DataFlow::Node { }

/**
* A default sink representing methods susceptible to URL
* forwarding attacks.
*/
private class DefaultUrlForwardSink extends UrlForwardSink {
DefaultUrlForwardSink() { sinkNode(this, "url-forward") }
}

/**
* An expression appended (perhaps indirectly) to `"forward:"`
* and reachable from a Spring entry point.
*/
private class SpringUrlForwardPrefixSink extends UrlForwardSink {
SpringUrlForwardPrefixSink() {
any(SpringRequestMappingMethod srmm).polyCalls*(this.getEnclosingCallable()) and
appendedToForwardPrefix(this)
}
}

pragma[nomagic]
private predicate appendedToForwardPrefix(DataFlow::ExprNode exprNode) {
exists(ForwardPrefix fp | exprNode.asExpr() = fp.getAnAppendedExpression())
}

private class ForwardPrefix extends InterestingPrefix {
ForwardPrefix() { this.getStringValue() = "forward:" }

override int getOffset() { result = 0 }
}

/** A URL forward barrier. */
abstract class UrlForwardBarrier extends DataFlow::Node { }

private class PrimitiveBarrier extends UrlForwardBarrier instanceof SimpleTypeSanitizer { }

/**
* A barrier for values appended to a "redirect:" prefix.
* These results are excluded because they should be handled
* by the `java/unvalidated-url-redirection` query instead.
*/
private class RedirectPrefixBarrier extends UrlForwardBarrier {
RedirectPrefixBarrier() { this.asExpr() = any(RedirectPrefix fp).getAnAppendedExpression() }
}

private class RedirectPrefix extends InterestingPrefix {
RedirectPrefix() { this.getStringValue() = "redirect:" }

override int getOffset() { result = 0 }
}

/**
* A value that is the result of prepending a string that prevents
* any value from controlling the path of a URL.
*/
private class FollowsBarrierPrefix extends UrlForwardBarrier {
FollowsBarrierPrefix() { this.asExpr() = any(BarrierPrefix fp).getAnAppendedExpression() }
}

private class BarrierPrefix extends InterestingPrefix {
int offset;

BarrierPrefix() {
// Matches strings that look like when prepended to untrusted input, they will restrict
// the path of a URL: for example, anything containing `?` or `#`.
exists(this.getStringValue().regexpFind("[?#]", 0, offset))
or
this.(CharacterLiteral).getValue() = ["?", "#"] and offset = 0
}

override int getOffset() { result = offset }
}

/**
* A barrier that protects against path injection vulnerabilities
* while accounting for URL encoding.
*/
private class UrlPathBarrier extends UrlForwardBarrier instanceof PathInjectionSanitizer {
UrlPathBarrier() {
this instanceof ExactPathMatchSanitizer or
this instanceof NoUrlEncodingBarrier or
this instanceof FullyDecodesUrlBarrier
}
}

/** A call to a method that decodes a URL. */
abstract class UrlDecodeCall extends MethodCall { }

private class DefaultUrlDecodeCall extends UrlDecodeCall {
DefaultUrlDecodeCall() {
this.getMethod() instanceof UrlDecodeMethod or
this.getMethod().hasQualifiedName("org.eclipse.jetty.util.URIUtil", "URIUtil", "decodePath")
}
}

/** A repeated call to a method that decodes a URL. */
abstract class RepeatedUrlDecodeCall extends MethodCall { }

private class DefaultRepeatedUrlDecodeCall extends RepeatedUrlDecodeCall instanceof UrlDecodeCall {
DefaultRepeatedUrlDecodeCall() { this.getAnEnclosingStmt() instanceof LoopStmt }
}

/** A method call that checks a string for URL encoding. */
abstract class CheckUrlEncodingCall extends MethodCall { }

private class DefaultCheckUrlEncodingCall extends CheckUrlEncodingCall {
DefaultCheckUrlEncodingCall() {
this.getMethod() instanceof StringContainsMethod and
this.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "%"
}
}

/** A guard that looks for a method call that checks for URL encoding. */
private class CheckUrlEncodingGuard extends Guard instanceof CheckUrlEncodingCall {
Expr getCheckedExpr() { result = this.(MethodCall).getQualifier() }
}

/** Holds if `g` is guard for a URL that does not contain URL encoding. */
private predicate noUrlEncodingGuard(Guard g, Expr e, boolean branch) {
e = g.(CheckUrlEncodingGuard).getCheckedExpr() and
branch = false
or
branch = false and
g.(Expr).getType() instanceof BooleanType and
(
exists(CheckUrlEncodingCall call, AssignExpr ae |
ae.getSource() = call and
e = call.getQualifier() and
g = ae.getDest()
)
or
exists(CheckUrlEncodingCall call, LocalVariableDeclExpr vde |
vde.getInitOrPatternSource() = call and
e = call.getQualifier() and
g = vde.getAnAccess()
)
)
}

/** A barrier for URLs that do not contain URL encoding. */
private class NoUrlEncodingBarrier extends DataFlow::Node {
NoUrlEncodingBarrier() { this = DataFlow::BarrierGuard<noUrlEncodingGuard/3>::getABarrierNode() }
}

/** Holds if `g` is guard for a URL that is fully decoded. */
private predicate fullyDecodesUrlGuard(Expr e) {
exists(CheckUrlEncodingGuard g, RepeatedUrlDecodeCall decodeCall |
e = g.getCheckedExpr() and
g.controls(decodeCall.getBasicBlock(), true)
)
}

/** A barrier for URLs that are fully decoded. */
private class FullyDecodesUrlBarrier extends DataFlow::Node {
FullyDecodesUrlBarrier() {
exists(Variable v, Expr e | this.asExpr() = v.getAnAccess() |
fullyDecodesUrlGuard(e) and
e = v.getAnAccess() and
e.getBasicBlock().bbDominates(this.asExpr().getBasicBlock())
)
}
}

/**
* A taint-tracking configuration for reasoning about URL forwarding.
*/
module UrlForwardFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof ThreatModelFlowSource and
// excluded due to FPs
not exists(MethodCall mc, Method m |
m instanceof HttpServletRequestGetRequestUriMethod or
m instanceof HttpServletRequestGetRequestUrlMethod or
m instanceof HttpServletRequestGetPathMethod
|
mc.getMethod() = m and
mc = source.asExpr()
)
}

predicate isSink(DataFlow::Node sink) { sink instanceof UrlForwardSink }

predicate isBarrier(DataFlow::Node node) { node instanceof UrlForwardBarrier }

DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
}

/**
* Taint-tracking flow for URL forwarding.
*/
module UrlForwardFlow = TaintTracking::Global<UrlForwardFlowConfig>;
17 changes: 17 additions & 0 deletions java/ql/src/Security/CWE/CWE-552/UrlForward.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
public class UrlForward extends HttpServlet {
private static final String VALID_FORWARD = "https://cwe.mitre.org/data/definitions/552.html";

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ServletConfig cfg = getServletConfig();
ServletContext sc = cfg.getServletContext();

// BAD: a request parameter is incorporated without validation into a URL forward
sc.getRequestDispatcher(request.getParameter("target")).forward(request, response);

// GOOD: the request parameter is validated against a known fixed string
if (VALID_FORWARD.equals(request.getParameter("target"))) {
sc.getRequestDispatcher(VALID_FORWARD).forward(request, response);
}
}
}
Loading