Merge pull request #14854 from jcogs33/jcogs33/unsafe-url-forward-promotion

Java: Promote Unsafe URL Forward query from experimental
This commit is contained in:
Jami
2024-03-29 16:34:06 -04:00
committed by GitHub
42 changed files with 758 additions and 1307 deletions

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,36 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Directly incorporating user input into a URL forward request without validating the input
can cause file information disclosure by allowing an attacker to access unauthorized URLs.</p>
</overview>
<recommendation>
<p>To guard against untrusted URL forwarding, you should avoid putting user input
directly into a forwarded URL. Instead, you should maintain a list of authorized
URLs on the server, then choose from that list based on the user input provided.</p>
</recommendation>
<example>
<p>The following example shows an HTTP request parameter being used directly in a URL forward
without validating the input, which may cause file information disclosure.
It also shows how to remedy the problem by validating the user input against a known fixed string.
</p>
<sample src="UrlForward.java" />
</example>
<references>
<li>OWASP:
<a href="https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html">Unvalidated Redirects and Forwards Cheat Sheet</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,21 @@
/**
* @name URL forward from a remote source
* @description URL forward based on unvalidated user input
* may cause file information disclosure.
* @kind path-problem
* @problem.severity error
* @security-severity 7.5
* @precision high
* @id java/unvalidated-url-forward
* @tags security
* external/cwe/cwe-552
*/
import java
import semmle.code.java.security.UrlForwardQuery
import UrlForwardFlow::PathGraph
from UrlForwardFlow::PathNode source, UrlForwardFlow::PathNode sink
where UrlForwardFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Untrusted URL forward depends on a $@.", source.getNode(),
"user-provided value"

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* The query `java/unsafe-url-forward-dispatch-load` has been promoted from experimental to the main query pack as `java/unvalidated-url-forward`. Its results will now appear by default. This query was originally submitted as an experimental query [by @haby0](https://github.com/github/codeql/pull/6240) and [by @luchua-bc](https://github.com/github/codeql/pull/7286).

View File

@@ -1,21 +0,0 @@
//BAD: no path validation in Spring resource loading
@GetMapping("/file")
public String getFileContent(@RequestParam(name="fileName") String fileName) {
ClassPathResource clr = new ClassPathResource(fileName);
File file = ResourceUtils.getFile(fileName);
Resource resource = resourceLoader.getResource(fileName);
}
//GOOD: check for a trusted prefix, ensuring path traversal is not used to erase that prefix in Spring resource loading:
@GetMapping("/file")
public String getFileContent(@RequestParam(name="fileName") String fileName) {
if (!fileName.contains("..") && fileName.hasPrefix("/public-content")) {
ClassPathResource clr = new ClassPathResource(fileName);
File file = ResourceUtils.getFile(fileName);
Resource resource = resourceLoader.getResource(fileName);
}
}

View File

@@ -1,18 +0,0 @@
// BAD: no URI validation
URL url = request.getServletContext().getResource(requestUrl);
url = getClass().getResource(requestUrl);
InputStream in = url.openStream();
InputStream in = request.getServletContext().getResourceAsStream(requestPath);
in = getClass().getClassLoader().getResourceAsStream(requestPath);
// GOOD: check for a trusted prefix, ensuring path traversal is not used to erase that prefix:
// (alternatively use `Path.normalize` instead of checking for `..`)
if (!requestPath.contains("..") && requestPath.startsWith("/trusted")) {
InputStream in = request.getServletContext().getResourceAsStream(requestPath);
}
Path path = Paths.get(requestUrl).normalize().toRealPath();
if (path.startsWith("/trusted")) {
URL url = request.getServletContext().getResource(path.toString());
}

View File

@@ -1,11 +0,0 @@
// BAD: no URI validation
String returnURL = request.getParameter("returnURL");
RequestDispatcher rd = sc.getRequestDispatcher(returnURL);
rd.forward(request, response);
// GOOD: check for a trusted prefix, ensuring path traversal is not used to erase that prefix:
// (alternatively use `Path.normalize` instead of checking for `..`)
if (!returnURL.contains("..") && returnURL.hasPrefix("/pages")) { ... }
// Also GOOD: check for a forbidden prefix, ensuring URL-encoding is not used to evade the check:
// (alternatively use `URLDecoder.decode` before `hasPrefix`)
if (returnURL.hasPrefix("/internal") && !returnURL.contains("%")) { ... }

View File

@@ -1,38 +0,0 @@
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class UnsafeUrlForward {
@GetMapping("/bad1")
public ModelAndView bad1(String url) {
return new ModelAndView(url);
}
@GetMapping("/bad2")
public void bad2(String url, HttpServletRequest request, HttpServletResponse response) {
try {
request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").include(request, response);
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
@GetMapping("/good1")
public void good1(String url, HttpServletRequest request, HttpServletResponse response) {
try {
request.getRequestDispatcher("/index.jsp?token=" + url).forward(request, response);
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -1,70 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Constructing a server-side redirect path with user input could allow an attacker to download application binaries
(including application classes or jar files) or view arbitrary files within protected directories.</p>
</overview>
<recommendation>
<p>Unsanitized user provided data must not be used to construct the path for URL forwarding. In order to prevent
untrusted URL forwarding, it is recommended to avoid concatenating user input directly into the forwarding URL.
Instead, user input should be checked against allowed (e.g., must come within <code>user_content/</code>) or disallowed
(e.g. must not come within <code>/internal</code>) paths, ensuring that neither path traversal using <code>../</code>
or URL encoding are used to evade these checks.
</p>
</recommendation>
<example>
<p>The following examples show the bad case and the good case respectively.
The <code>bad</code> methods show an HTTP request parameter being used directly in a URL forward
without validating the input, which may cause file leakage. In the <code>good1</code> method,
ordinary forwarding requests are shown, which will not cause file leakage.
</p>
<sample src="UnsafeUrlForward.java" />
<p>The following examples show an HTTP request parameter or request path being used directly in a
request dispatcher of Java EE without validating the input, which allows sensitive file exposure
attacks. It also shows how to remedy the problem by validating the user input.
</p>
<sample src="UnsafeServletRequestDispatch.java" />
<p>The following examples show an HTTP request parameter or request path being used directly to
retrieve a resource of a Java EE application without validating the input, which allows sensitive
file exposure attacks. It also shows how to remedy the problem by validating the user input.
</p>
<sample src="UnsafeResourceGet.java" />
<p>The following examples show an HTTP request parameter being used directly to retrieve a resource
of a Java Spring application without validating the input, which allows sensitive file exposure
attacks. It also shows how to remedy the problem by validating the user input.
</p>
<sample src="UnsafeLoadSpringResource.java" />
</example>
<references>
<li>File Disclosure:
<a href="https://vulncat.fortify.com/en/detail?id=desc.dataflow.java.file_disclosure_spring">Unsafe Url Forward</a>.
</li>
<li>Jakarta Javadoc:
<a href="https://jakarta.ee/specifications/webprofile/9/apidocs/jakarta/servlet/servletrequest#getRequestDispatcher-java.lang.String-">Security vulnerability with unsafe usage of RequestDispatcher</a>.
</li>
<li>Micro Focus:
<a href="https://vulncat.fortify.com/en/detail?id=desc.dataflow.java.file_disclosure_j2ee">File Disclosure: J2EE</a>
</li>
<li>CVE-2015-5174:
<a href="https://vuldb.com/?id.81084">Apache Tomcat 6.0/7.0/8.0/9.0 Servletcontext getResource/getResourceAsStream/getResourcePaths Path Traversal</a>
</li>
<li>CVE-2019-3799:
<a href="https://github.com/mpgn/CVE-2019-3799">CVE-2019-3799 - Spring-Cloud-Config-Server Directory Traversal &lt; 2.1.2, 2.0.4, 1.4.6</a>
</li>
</references>
</qhelp>

View File

@@ -1,64 +0,0 @@
/**
* @name Unsafe URL forward, dispatch, or load from remote source
* @description URL forward, dispatch, or load based on unvalidated user-input
* may cause file information disclosure.
* @kind path-problem
* @problem.severity error
* @precision high
* @id java/unsafe-url-forward-dispatch-load
* @tags security
* experimental
* external/cwe/cwe-552
*/
import java
import UnsafeUrlForward
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import experimental.semmle.code.java.frameworks.Jsf
import semmle.code.java.security.PathSanitizer
import UnsafeUrlForwardFlow::PathGraph
module UnsafeUrlForwardFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof ThreatModelFlowSource and
not exists(MethodCall ma, Method m | ma.getMethod() = m |
(
m instanceof HttpServletRequestGetRequestUriMethod or
m instanceof HttpServletRequestGetRequestUrlMethod or
m instanceof HttpServletRequestGetPathMethod
) and
ma = source.asExpr()
)
}
predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeUrlForwardSink }
predicate isBarrier(DataFlow::Node node) {
node instanceof UnsafeUrlForwardSanitizer or
node instanceof PathInjectionSanitizer
}
DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
predicate isAdditionalFlowStep(DataFlow::Node prev, DataFlow::Node succ) {
exists(MethodCall ma |
(
ma.getMethod() instanceof GetServletResourceMethod or
ma.getMethod() instanceof GetFacesResourceMethod or
ma.getMethod() instanceof GetClassResourceMethod or
ma.getMethod() instanceof GetClassLoaderResourceMethod or
ma.getMethod() instanceof GetWildflyResourceMethod
) and
ma.getArgument(0) = prev.asExpr() and
ma = succ.asExpr()
)
}
}
module UnsafeUrlForwardFlow = TaintTracking::Global<UnsafeUrlForwardFlowConfig>;
from UnsafeUrlForwardFlow::PathNode source, UnsafeUrlForwardFlow::PathNode sink
where UnsafeUrlForwardFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Potentially untrusted URL forward due to $@.",
source.getNode(), "user-provided value"

View File

@@ -1,163 +0,0 @@
import java
private import experimental.semmle.code.java.frameworks.Jsf
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.frameworks.javaee.ejb.EJBRestrictions
private import experimental.semmle.code.java.frameworks.SpringResource
private import semmle.code.java.security.Sanitizers
private class ActiveModels extends ActiveExperimentalModels {
ActiveModels() { this = "unsafe-url-forward" }
}
/** A sink for unsafe URL forward vulnerabilities. */
abstract class UnsafeUrlForwardSink extends DataFlow::Node { }
/** A sanitizer for unsafe URL forward vulnerabilities. */
abstract class UnsafeUrlForwardSanitizer extends DataFlow::Node { }
/** An argument to `getRequestDispatcher`. */
private class RequestDispatcherSink extends UnsafeUrlForwardSink {
RequestDispatcherSink() {
exists(MethodCall ma |
ma.getMethod() instanceof GetRequestDispatcherMethod and
ma.getArgument(0) = this.asExpr()
)
}
}
/** The `getResource` method of `Class`. */
class GetClassResourceMethod extends Method {
GetClassResourceMethod() {
this.getDeclaringType() instanceof TypeClass and
this.hasName("getResource")
}
}
/** The `getResourceAsStream` method of `Class`. */
class GetClassResourceAsStreamMethod extends Method {
GetClassResourceAsStreamMethod() {
this.getDeclaringType() instanceof TypeClass and
this.hasName("getResourceAsStream")
}
}
/** The `getResource` method of `ClassLoader`. */
class GetClassLoaderResourceMethod extends Method {
GetClassLoaderResourceMethod() {
this.getDeclaringType() instanceof ClassLoaderClass and
this.hasName("getResource")
}
}
/** The `getResourceAsStream` method of `ClassLoader`. */
class GetClassLoaderResourceAsStreamMethod extends Method {
GetClassLoaderResourceAsStreamMethod() {
this.getDeclaringType() instanceof ClassLoaderClass and
this.hasName("getResourceAsStream")
}
}
/** The JBoss class `FileResourceManager`. */
class FileResourceManager extends RefType {
FileResourceManager() {
this.hasQualifiedName("io.undertow.server.handlers.resource", "FileResourceManager")
}
}
/** The JBoss method `getResource` of `FileResourceManager`. */
class GetWildflyResourceMethod extends Method {
GetWildflyResourceMethod() {
this.getDeclaringType().getASupertype*() instanceof FileResourceManager and
this.hasName("getResource")
}
}
/** The JBoss class `VirtualFile`. */
class VirtualFile extends RefType {
VirtualFile() { this.hasQualifiedName("org.jboss.vfs", "VirtualFile") }
}
/** The JBoss method `getChild` of `FileResourceManager`. */
class GetVirtualFileChildMethod extends Method {
GetVirtualFileChildMethod() {
this.getDeclaringType().getASupertype*() instanceof VirtualFile and
this.hasName("getChild")
}
}
/** An argument to `getResource()` or `getResourceAsStream()`. */
private class GetResourceSink extends UnsafeUrlForwardSink {
GetResourceSink() {
sinkNode(this, "request-forgery")
or
sinkNode(this, "get-resource")
or
exists(MethodCall ma |
(
ma.getMethod() instanceof GetServletResourceAsStreamMethod or
ma.getMethod() instanceof GetFacesResourceAsStreamMethod or
ma.getMethod() instanceof GetClassResourceAsStreamMethod or
ma.getMethod() instanceof GetClassLoaderResourceAsStreamMethod or
ma.getMethod() instanceof GetVirtualFileChildMethod
) and
ma.getArgument(0) = this.asExpr()
)
}
}
/** A sink for methods that load Spring resources. */
private class SpringResourceSink extends UnsafeUrlForwardSink {
SpringResourceSink() {
exists(MethodCall ma |
ma.getMethod() instanceof GetResourceUtilsMethod and
ma.getArgument(0) = this.asExpr()
)
}
}
/** An argument to `new ModelAndView` or `ModelAndView.setViewName`. */
private class SpringModelAndViewSink extends UnsafeUrlForwardSink {
SpringModelAndViewSink() {
exists(ClassInstanceExpr cie |
cie.getConstructedType() instanceof ModelAndView and
cie.getArgument(0) = this.asExpr()
)
or
exists(SpringModelAndViewSetViewNameCall smavsvnc | smavsvnc.getArgument(0) = this.asExpr())
}
}
private class PrimitiveSanitizer extends UnsafeUrlForwardSanitizer instanceof SimpleTypeSanitizer {
}
private class SanitizingPrefix extends InterestingPrefix {
SanitizingPrefix() {
not this.getStringValue().matches("/WEB-INF/%") and
not this.getStringValue() = "forward:"
}
override int getOffset() { result = 0 }
}
private class FollowsSanitizingPrefix extends UnsafeUrlForwardSanitizer {
FollowsSanitizingPrefix() { this.asExpr() = any(SanitizingPrefix fp).getAnAppendedExpression() }
}
private class ForwardPrefix extends InterestingPrefix {
ForwardPrefix() { this.getStringValue() = "forward:" }
override int getOffset() { result = 0 }
}
/**
* An expression appended (perhaps indirectly) to `"forward:"`, and which
* is reachable from a Spring entry point.
*/
private class SpringUrlForwardSink extends UnsafeUrlForwardSink {
SpringUrlForwardSink() {
any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable()) and
this.asExpr() = any(ForwardPrefix fp).getAnAppendedExpression()
}
}