From 0d38a9625e039120e715dfe79b529dc5bfead3d8 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 20 Nov 2023 15:12:33 -0500
Subject: [PATCH 01/40] Java: copy files from experimental
---
...unsafeUrlForwardExperimentalMove.model.yml | 61 ++++
.../CWE/CWE-552/UnsafeLoadSpringResource.java | 21 ++
.../CWE/CWE-552/UnsafeResourceGet.java | 18 ++
.../CWE-552/UnsafeServletRequestDispatch.java | 11 +
.../CWE/CWE-552/UnsafeUrlForward.java | 38 +++
.../CWE/CWE-552/UnsafeUrlForward.qhelp | 70 +++++
.../Security/CWE/CWE-552/UnsafeUrlForward.ql | 63 ++++
.../Security/CWE/CWE-552/UnsafeUrlForward.qll | 163 +++++++++++
.../CWE-552/UnsafeLoadSpringResource.java | 155 ++++++++++
.../security/CWE-552/UnsafeRequestPath.java | 52 ++++
.../security/CWE-552/UnsafeResourceGet.java | 270 ++++++++++++++++++
.../security/CWE-552/UnsafeResourceGet2.java | 58 ++++
.../CWE-552/UnsafeServletRequestDispatch.java | 131 +++++++++
.../CWE-552/UnsafeUrlForward.expected | 129 +++++++++
.../security/CWE-552/UnsafeUrlForward.java | 78 +++++
.../security/CWE-552/UnsafeUrlForward.qlref | 1 +
.../test/query-tests/security/CWE-552/options | 1 +
17 files changed, 1320 insertions(+)
create mode 100644 java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml
create mode 100644 java/ql/src/Security/CWE/CWE-552/UnsafeLoadSpringResource.java
create mode 100644 java/ql/src/Security/CWE/CWE-552/UnsafeResourceGet.java
create mode 100644 java/ql/src/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java
create mode 100644 java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.java
create mode 100644 java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qhelp
create mode 100644 java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
create mode 100644 java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qll
create mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
create mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
create mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java
create mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java
create mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
create mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.expected
create mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java
create mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.qlref
create mode 100644 java/ql/test/query-tests/security/CWE-552/options
diff --git a/java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml b/java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml
new file mode 100644
index 000000000000..b48d891e692d
--- /dev/null
+++ b/java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml
@@ -0,0 +1,61 @@
+extensions:
+ - addsTo:
+ pack: codeql/java-all
+ extensible: sourceModel
+ data:
+ - ["jakarta.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual"]
+ - ["javax.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual"]
+
+ # # ! below added by me when debugging CVEs:
+ # - ["org.springframework.cloud.config.server.resource", "ResourceController", True, "retrieve", "(String,String,String,ServletWebRequest,boolean)", "", "Parameter[3]", "remote", "manual"]
+ # - ["org.springframework.web.context.request", "ServletWebRequest", True, "getContextPath", "()", "", "ReturnValue", "remote", "manual"]
+
+ - addsTo:
+ pack: codeql/java-all
+ extensible: sinkModel
+ data:
+ - ["java.util.concurrent", "TimeUnit", True, "sleep", "", "", "Argument[0]", "thread-pause", "manual"] # ! this seems like a typo; doesn't look like it's used in the query at all
+
+ - ["org.springframework.core.io", "ClassPathResource", True, "getFilename", "", "", "Argument[this]", "get-resource", "manual"] # ! Note: `ClassPathResource` implements `Resource`, so it might make more sense to model some of these as `Resource` with subtype True.
+ - ["org.springframework.core.io", "ClassPathResource", True, "getPath", "", "", "Argument[this]", "get-resource", "manual"]
+ - ["org.springframework.core.io", "ClassPathResource", True, "getURL", "", "", "Argument[this]", "get-resource", "manual"]
+ - ["org.springframework.core.io", "ClassPathResource", True, "resolveURL", "", "", "Argument[this]", "get-resource", "manual"]
+ # # ! below added by me when debugging CVEs:
+ # - ["org.springframework.cloud.config.server.resource", "ResourceController", True, "retrieve", "", "", "Argument[0]", "get-resource", "manual"] # don't need
+ # # - ["org.springframework.cloud.config.server.resource", "ResourceController", True, "getFilePath", "", "", "Argument[0..3]", "get-resource", "manual"] # don't need
+ # # - ["org.springframework.cloud.config.server.resource", "ResourceRepository", True, "findOne", "", "", "Argument[0..3]", "get-resource", "manual"] # convert to summary
+ # # - ["org.springframework.core.io", "InputStreamSource", True, "getInputStream", "", "", "Argument[this]", "get-resource", "manual"] # convert to summary
+ # - ["org.springframework.util", "StreamUtils", True, "copyToString", "", "", "Argument[0]", "get-resource", "manual"] # * public class with good docs
+ # # - ["org.springframework.cloud.config.server.environment", "SearchPathLocator", True, "getLocations", "(String,String,String)", "", "Argument[0..2]", "get-resource", "manual"] # convert to summary
+ # # - ["org.springframework.cloud.config.server.environment", "SearchPathLocator$Locations", True, "getLocations", "()", "", "Argument[this]", "get-resource", "manual"] # convert to summary
+ # - ["org.springframework.core.io", "ResourceLoader", True, "getResource", "", "", "Argument[0]", "get-resource", "manual"] # * public interface with good docs, might be problematic for FPs based on fact that the ext contributor changed this to a taint step to avoid "exists" FPS (maybe there's another way to exclude those FPs though).
+ # - ["javax.servlet", "ServletContext", True, "getResource", "", "", "Argument[0]", "get-resource", "manual"]
+ # - ["javax.servlet", "ServletContext", True, "getResourceAsStream", "", "", "Argument[0]", "get-resource", "manual"]
+ # - ["javax.servlet", "ServletContext", True, "getResourcePaths", "", "", "Argument[0]", "get-resource", "manual"]
+ # - ["javax.servlet", "ServletContext", True, "getResource", "", "", "Argument[this]", "get-resource", "manual"]
+ # - ["javax.servlet", "ServletContext", True, "getResourceAsStream", "", "", "Argument[this]", "get-resource", "manual"]
+ # - ["javax.servlet", "ServletContext", True, "getResourcePaths", "", "", "Argument[this]", "get-resource", "manual"]
+ # # - ["org.apache.tomcat.util.http", "RequestUtil", True, "normalize", "", "", "Argument[0]", "get-resource", "manual"]
+
+ - addsTo:
+ pack: codeql/java-all
+ extensible: summaryModel
+ data:
+ - ["io.undertow.server.handlers.resource", "Resource", True, "getFile", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
+ - ["io.undertow.server.handlers.resource", "Resource", True, "getFilePath", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
+ - ["io.undertow.server.handlers.resource", "Resource", True, "getPath", "", "", "Argument[this]", "ReturnValue", "taint", "manual"] # ! this as a taint step seems to contradict the fact that they did `ClassPathResource.getPath` as a sink for Spring...
+ - ["java.nio.file", "Path", True, "normalize", "", "", "Argument[this]", "ReturnValue", "taint", "manual"] # ! shouldn't this be a sanitizer instead??? Or no because WEB-INF ones don't care about normalization?
+ - ["java.nio.file", "Path", True, "resolve", "", "", "Argument[this]", "ReturnValue", "taint", "manual"] # ! check if this and the below are already in the default models
+ - ["java.nio.file", "Path", True, "resolve", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
+ - ["java.nio.file", "Path", True, "toString", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
+ - ["java.nio.file", "Paths", True, "get", "", "", "Argument[0..1]", "ReturnValue", "taint", "manual"]
+
+ - ["org.springframework.core.io", "ClassPathResource", False, "ClassPathResource", "", "", "Argument[0]", "Argument[this]", "taint", "manual"]
+ - ["org.springframework.core.io", "Resource", True, "createRelative", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
+ # # ! below added/modified by me when debugging CVEs:
+ - ["org.springframework.core.io", "ResourceLoader", True, "getResource", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
+ # - ["org.springframework.cloud.config.server.resource", "ResourceRepository", True, "findOne", "", "", "Argument[0..3]", "ReturnValue", "taint", "manual"] # * public interface, but might be too specific, no easily findable docs...
+ # - ["org.springframework.core.io", "InputStreamSource", True, "getInputStream", "", "", "Argument[this]", "ReturnValue", "taint", "manual"] # * public interface with good docs, Note: other `getInputStream`s are remote source and/or taint step, so this as taint step versus sink probably is more consistent
+ # - ["org.springframework.cloud.config.server.environment", "SearchPathLocator", True, "getLocations", "(String,String,String)", "", "Argument[0..2]", "ReturnValue", "taint", "manual"] # * public interface with docs: https://www.javadoc.io/static/org.springframework.cloud/spring-cloud-config-server/2.1.0.RELEASE/org/springframework/cloud/config/server/environment/SearchPathLocator.html
+ # - ["org.springframework.cloud.config.server.environment", "SearchPathLocator$Locations", True, "getLocations", "()", "", "Argument[this]", "ReturnValue", "taint", "manual"] # ! is the `Locations` class package-private? Or does it inherit public from it's enclosing interface?
+ # - ["org.springframework.cloud.config.server.resource", "ResourceController", True, "getFilePath", "", "", "Argument[0..3]", "ReturnValue", "taint", "manual"] # don't need
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeLoadSpringResource.java b/java/ql/src/Security/CWE/CWE-552/UnsafeLoadSpringResource.java
new file mode 100644
index 000000000000..ce462fe490ef
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-552/UnsafeLoadSpringResource.java
@@ -0,0 +1,21 @@
+//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);
+ }
+}
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeResourceGet.java b/java/ql/src/Security/CWE/CWE-552/UnsafeResourceGet.java
new file mode 100644
index 000000000000..8b3583bf59e2
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-552/UnsafeResourceGet.java
@@ -0,0 +1,18 @@
+// 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());
+}
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java b/java/ql/src/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java
new file mode 100644
index 000000000000..a2bbf3dfcd85
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java
@@ -0,0 +1,11 @@
+// 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("%")) { ... }
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.java b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.java
new file mode 100644
index 000000000000..d159c4057362
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.java
@@ -0,0 +1,38 @@
+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();
+ }
+ }
+}
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qhelp b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qhelp
new file mode 100644
index 000000000000..2e425952edc3
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qhelp
@@ -0,0 +1,70 @@
+
+
+
+
+
+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.
+
+
+
+
+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 user_content/) or disallowed
+(e.g. must not come within /internal) paths, ensuring that neither path traversal using ../
+or URL encoding are used to evade these checks.
+
+
+
+
+
+The following examples show the bad case and the good case respectively.
+The bad methods show an HTTP request parameter being used directly in a URL forward
+without validating the input, which may cause file leakage. In the good1 method,
+ordinary forwarding requests are shown, which will not cause file leakage.
+
+
+
+
+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.
+
+
+
+
+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.
+
+
+
+
+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.
+
+
+
+
+
+File Disclosure:
+ Unsafe Url Forward.
+
+Jakarta Javadoc:
+ Security vulnerability with unsafe usage of RequestDispatcher.
+
+Micro Focus:
+ File Disclosure: J2EE
+
+CVE-2015-5174:
+ Apache Tomcat 6.0/7.0/8.0/9.0 Servletcontext getResource/getResourceAsStream/getResourcePaths Path Traversal
+
+CVE-2019-3799:
+ CVE-2019-3799 - Spring-Cloud-Config-Server Directory Traversal < 2.1.2, 2.0.4, 1.4.6
+
+
+
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
new file mode 100644
index 000000000000..240023f9ffc0
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
@@ -0,0 +1,63 @@
+/**
+ * @name Unsafe URL forward or include from a remote source
+ * @description URL forward or include based on unvalidated user-input
+ * may cause file information disclosure.
+ * @kind path-problem
+ * @problem.severity error
+ * @precision high
+ * @id java/unsafe-url-forward-include
+ * @tags security
+ * external/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;
+
+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"
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qll b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qll
new file mode 100644
index 000000000000..db610eb65cec
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qll
@@ -0,0 +1,163 @@
+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
+
+/** 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 {
+ PrimitiveSanitizer() {
+ this.getType() instanceof PrimitiveType or
+ this.getType() instanceof BoxedType or
+ this.getType() instanceof NumberType
+ }
+}
+
+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()
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java b/java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
new file mode 100644
index 000000000000..c7e114aede35
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
@@ -0,0 +1,155 @@
+package com.example;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.file.Files;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.util.ResourceUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/** Sample class of Spring RestController */
+@RestController
+public class UnsafeLoadSpringResource {
+ @GetMapping("/file1")
+ //BAD: Get resource from ClassPathResource without input validation
+ public String getFileContent1(@RequestParam(name="fileName") String fileName) {
+ // A request such as the following can disclose source code and application configuration
+ // fileName=/../../WEB-INF/views/page.jsp
+ // fileName=/com/example/package/SampleController.class
+ ClassPathResource clr = new ClassPathResource(fileName);
+ char[] buffer = new char[4096];
+ StringBuilder out = new StringBuilder();
+ try {
+ Reader in = new FileReader(clr.getFilename());
+ for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
+ out.append(buffer, 0, numRead);
+ }
+ } catch (IOException ie) {
+ ie.printStackTrace();
+ }
+ return out.toString();
+ }
+
+ @GetMapping("/file1a")
+ //GOOD: Get resource from ClassPathResource with input path validation
+ public String getFileContent1a(@RequestParam(name="fileName") String fileName) {
+ String result = null;
+ if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
+ ClassPathResource clr = new ClassPathResource(fileName);
+ char[] buffer = new char[4096];
+ StringBuilder out = new StringBuilder();
+ try {
+ Reader in = new InputStreamReader(clr.getInputStream(), "UTF-8");
+ for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
+ out.append(buffer, 0, numRead);
+ }
+ } catch (IOException ie) {
+ ie.printStackTrace();
+ }
+ result = out.toString();
+ }
+ return result;
+ }
+
+ @GetMapping("/file2")
+ //BAD: Get resource from ResourceUtils without input validation
+ public String getFileContent2(@RequestParam(name="fileName") String fileName) {
+ String content = null;
+
+ try {
+ // A request such as the following can disclose source code and system configuration
+ // fileName=/etc/hosts
+ // fileName=file:/etc/hosts
+ // fileName=/opt/appdir/WEB-INF/views/page.jsp
+ File file = ResourceUtils.getFile(fileName);
+ //Read File Content
+ content = new String(Files.readAllBytes(file.toPath()));
+ } catch (IOException ie) {
+ ie.printStackTrace();
+ }
+ return content;
+ }
+
+ @GetMapping("/file2a")
+ //GOOD: Get resource from ResourceUtils with input path validation
+ public String getFileContent2a(@RequestParam(name="fileName") String fileName) {
+ String content = null;
+
+ if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
+ try {
+ File file = ResourceUtils.getFile(fileName);
+ //Read File Content
+ content = new String(Files.readAllBytes(file.toPath()));
+ } catch (IOException ie) {
+ ie.printStackTrace();
+ }
+ }
+ return content;
+ }
+
+ @Autowired
+ ResourceLoader resourceLoader;
+
+ @GetMapping("/file3")
+ //BAD: Get resource from ResourceLoader (same as application context) without input validation
+ // Note it is not detected without the generic `resource.getInputStream()` check
+ public String getFileContent3(@RequestParam(name="fileName") String fileName) {
+ String content = null;
+
+ try {
+ // A request such as the following can disclose source code and system configuration
+ // fileName=/WEB-INF/views/page.jsp
+ // fileName=/WEB-INF/classes/com/example/package/SampleController.class
+ // fileName=file:/etc/hosts
+ Resource resource = resourceLoader.getResource(fileName);
+
+ char[] buffer = new char[4096];
+ StringBuilder out = new StringBuilder();
+
+ Reader in = new InputStreamReader(resource.getInputStream(), "UTF-8");
+ for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
+ out.append(buffer, 0, numRead);
+ }
+ content = out.toString();
+ } catch (IOException ie) {
+ ie.printStackTrace();
+ }
+ return content;
+ }
+
+ @GetMapping("/file3a")
+ //GOOD: Get resource from ResourceLoader (same as application context) with input path validation
+ public String getFileContent3a(@RequestParam(name="fileName") String fileName) {
+ String content = null;
+
+ if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
+ try {
+ Resource resource = resourceLoader.getResource(fileName);
+
+ char[] buffer = new char[4096];
+ StringBuilder out = new StringBuilder();
+
+ Reader in = new InputStreamReader(resource.getInputStream(), "UTF-8");
+ for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
+ out.append(buffer, 0, numRead);
+ }
+ content = out.toString();
+ } catch (IOException ie) {
+ ie.printStackTrace();
+ }
+ }
+ return content;
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java b/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
new file mode 100644
index 000000000000..2de0cae0d3c5
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
@@ -0,0 +1,52 @@
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// @WebFilter("/*")
+public class UnsafeRequestPath implements Filter {
+ private static final String BASE_PATH = "/pages";
+
+ @Override
+ // BAD: Request dispatcher from servlet path without check
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ String path = ((HttpServletRequest) request).getServletPath();
+ // A sample payload "/%57EB-INF/web.xml" can bypass this `startsWith` check
+ if (path != null && !path.startsWith("/WEB-INF")) {
+ request.getRequestDispatcher(path).forward(request, response);
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+
+ // GOOD: Request dispatcher from servlet path with check
+ public void doFilter2(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ String path = ((HttpServletRequest) request).getServletPath();
+
+ if (path.startsWith(BASE_PATH) && !path.contains("..")) {
+ request.getRequestDispatcher(path).forward(request, response);
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+
+ // GOOD: Request dispatcher from servlet path with whitelisted string comparison
+ public void doFilter3(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ String path = ((HttpServletRequest) request).getServletPath();
+
+ if (path.equals("/comaction")) {
+ request.getRequestDispatcher(path).forward(request, response);
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java b/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java
new file mode 100644
index 000000000000..64c23334f187
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java
@@ -0,0 +1,270 @@
+package com.example;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.net.URI;
+import java.net.URL;
+import java.net.URISyntaxException;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletException;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+
+import io.undertow.server.handlers.resource.FileResourceManager;
+import io.undertow.server.handlers.resource.Resource;
+import org.jboss.vfs.VFS;
+import org.jboss.vfs.VirtualFile;
+
+public class UnsafeResourceGet extends HttpServlet {
+ private static final String BASE_PATH = "/pages";
+
+ @Override
+ // BAD: getResource constructed from `ServletContext` without input validation
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestUrl = request.getParameter("requestURL");
+ ServletOutputStream out = response.getOutputStream();
+
+ ServletConfig cfg = getServletConfig();
+ ServletContext sc = cfg.getServletContext();
+
+ // A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
+ URL url = sc.getResource(requestUrl);
+
+ InputStream in = url.openStream();
+ byte[] buf = new byte[4 * 1024]; // 4K buffer
+ int bytesRead;
+ while ((bytesRead = in.read(buf)) != -1) {
+ out.write(buf, 0, bytesRead);
+ }
+ }
+
+ // GOOD: getResource constructed from `ServletContext` with input validation
+ protected void doGetGood(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestUrl = request.getParameter("requestURL");
+ ServletOutputStream out = response.getOutputStream();
+
+ ServletConfig cfg = getServletConfig();
+ ServletContext sc = cfg.getServletContext();
+
+ Path path = Paths.get(requestUrl).normalize().toRealPath();
+ if (path.startsWith(BASE_PATH)) {
+ URL url = sc.getResource(path.toString());
+
+ InputStream in = url.openStream();
+ byte[] buf = new byte[4 * 1024]; // 4K buffer
+ int bytesRead;
+ while ((bytesRead = in.read(buf)) != -1) {
+ out.write(buf, 0, bytesRead);
+ }
+ }
+ }
+
+ // GOOD: getResource constructed from `ServletContext` with null check only
+ protected void doGetGood2(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestUrl = request.getParameter("requestURL");
+ PrintWriter writer = response.getWriter();
+
+ ServletConfig cfg = getServletConfig();
+ ServletContext sc = cfg.getServletContext();
+
+ // A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
+ URL url = sc.getResource(requestUrl);
+ if (url == null) {
+ writer.println("Requested source not found");
+ }
+ }
+
+ // GOOD: getResource constructed from `ServletContext` with `equals` check
+ protected void doGetGood3(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestUrl = request.getParameter("requestURL");
+ ServletOutputStream out = response.getOutputStream();
+
+ ServletContext sc = request.getServletContext();
+
+ if (requestUrl.equals("/public/crossdomain.xml")) {
+ URL url = sc.getResource(requestUrl);
+
+ InputStream in = url.openStream();
+ byte[] buf = new byte[4 * 1024]; // 4K buffer
+ int bytesRead;
+ while ((bytesRead = in.read(buf)) != -1) {
+ out.write(buf, 0, bytesRead);
+ }
+ }
+ }
+
+ @Override
+ // BAD: getResourceAsStream constructed from `ServletContext` without input validation
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestPath = request.getParameter("requestPath");
+ ServletOutputStream out = response.getOutputStream();
+
+ // A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
+ InputStream in = request.getServletContext().getResourceAsStream(requestPath);
+ byte[] buf = new byte[4 * 1024]; // 4K buffer
+ int bytesRead;
+ while ((bytesRead = in.read(buf)) != -1) {
+ out.write(buf, 0, bytesRead);
+ }
+ }
+
+ // GOOD: getResourceAsStream constructed from `ServletContext` with input validation
+ protected void doPostGood(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestPath = request.getParameter("requestPath");
+ ServletOutputStream out = response.getOutputStream();
+
+ if (!requestPath.contains("..") && requestPath.startsWith("/trusted")) {
+ InputStream in = request.getServletContext().getResourceAsStream(requestPath);
+ byte[] buf = new byte[4 * 1024]; // 4K buffer
+ int bytesRead;
+ while ((bytesRead = in.read(buf)) != -1) {
+ out.write(buf, 0, bytesRead);
+ }
+ }
+ }
+
+ @Override
+ // BAD: getResource constructed from `Class` without input validation
+ protected void doHead(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestUrl = request.getParameter("requestURL");
+ ServletOutputStream out = response.getOutputStream();
+
+ // A sample request /fake.jsp/../../../WEB-INF/web.xml can load the web.xml file
+ // Note the class is in two levels of subpackages and `Class.getResource` starts from its own directory
+ URL url = getClass().getResource(requestUrl);
+
+ InputStream in = url.openStream();
+ byte[] buf = new byte[4 * 1024]; // 4K buffer
+ int bytesRead;
+ while ((bytesRead = in.read(buf)) != -1) {
+ out.write(buf, 0, bytesRead);
+ }
+ }
+
+ // GOOD: getResource constructed from `Class` with input validation
+ protected void doHeadGood(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestUrl = request.getParameter("requestURL");
+ ServletOutputStream out = response.getOutputStream();
+
+ Path path = Paths.get(requestUrl).normalize().toRealPath();
+ if (path.startsWith(BASE_PATH)) {
+ URL url = getClass().getResource(path.toString());
+
+ InputStream in = url.openStream();
+ byte[] buf = new byte[4 * 1024]; // 4K buffer
+ int bytesRead;
+ while ((bytesRead = in.read(buf)) != -1) {
+ out.write(buf, 0, bytesRead);
+ }
+ }
+ }
+
+ @Override
+ // BAD: getResourceAsStream constructed from `ClassLoader` without input validation
+ protected void doPut(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestPath = request.getParameter("requestPath");
+ ServletOutputStream out = response.getOutputStream();
+
+ ServletConfig cfg = getServletConfig();
+ ServletContext sc = cfg.getServletContext();
+
+ // A sample request /fake.jsp/../../../WEB-INF/web.xml can load the web.xml file
+ // Note the class is in two levels of subpackages and `ClassLoader.getResourceAsStream` starts from its own directory
+ InputStream in = getClass().getClassLoader().getResourceAsStream(requestPath);
+ byte[] buf = new byte[4 * 1024]; // 4K buffer
+ int bytesRead;
+ while ((bytesRead = in.read(buf)) != -1) {
+ out.write(buf, 0, bytesRead);
+ }
+ }
+
+ // GOOD: getResourceAsStream constructed from `ClassLoader` with input validation
+ protected void doPutGood(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestPath = request.getParameter("requestPath");
+ ServletOutputStream out = response.getOutputStream();
+
+ ServletConfig cfg = getServletConfig();
+ ServletContext sc = cfg.getServletContext();
+
+ if (!requestPath.contains("..") && requestPath.startsWith("/trusted")) {
+ InputStream in = getClass().getClassLoader().getResourceAsStream(requestPath);
+ byte[] buf = new byte[4 * 1024]; // 4K buffer
+ int bytesRead;
+ while ((bytesRead = in.read(buf)) != -1) {
+ out.write(buf, 0, bytesRead);
+ }
+ }
+ }
+
+ // BAD: getResource constructed from `ClassLoader` without input validation
+ protected void doPutBad(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestUrl = request.getParameter("requestURL");
+ ServletOutputStream out = response.getOutputStream();
+
+ // A sample request /fake.jsp/../../../WEB-INF/web.xml can load the web.xml file
+ // Note the class is in two levels of subpackages and `ClassLoader.getResource` starts from its own directory
+ URL url = getClass().getClassLoader().getResource(requestUrl);
+
+ InputStream in = url.openStream();
+ byte[] buf = new byte[4 * 1024]; // 4K buffer
+ int bytesRead;
+ while ((bytesRead = in.read(buf)) != -1) {
+ out.write(buf, 0, bytesRead);
+ }
+ }
+
+ // BAD: getResource constructed using Undertow IO without input validation
+ protected void doPutBad2(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestPath = request.getParameter("requestPath");
+
+ try {
+ FileResourceManager rm = new FileResourceManager(VFS.getChild(new URI("/usr/share")).getPhysicalFile());
+ Resource rs = rm.getResource(requestPath);
+
+ VirtualFile overlay = VFS.getChild(new URI("EAP_HOME/modules/"));
+ // Do file operations
+ overlay.getChild(rs.getPath());
+ } catch (URISyntaxException ue) {
+ throw new IOException("Cannot parse the URI");
+ }
+ }
+
+ // GOOD: getResource constructed using Undertow IO with input validation
+ protected void doPutGood2(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String requestPath = request.getParameter("requestPath");
+
+ try {
+ FileResourceManager rm = new FileResourceManager(VFS.getChild(new URI("/usr/share")).getPhysicalFile());
+ Resource rs = rm.getResource(requestPath);
+
+ VirtualFile overlay = VFS.getChild(new URI("EAP_HOME/modules/"));
+ String path = rs.getPath();
+ if (path.startsWith("/trusted_path") && !path.contains("..")) {
+ // Do file operations
+ overlay.getChild(path);
+ }
+ } catch (URISyntaxException ue) {
+ throw new IOException("Cannot parse the URI");
+ }
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java b/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java
new file mode 100644
index 000000000000..b3d041d024cf
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java
@@ -0,0 +1,58 @@
+package com.example;
+
+import javax.faces.context.FacesContext;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Map;
+
+/** Sample class of JSF managed bean */
+public class UnsafeResourceGet2 {
+ // BAD: getResourceAsStream constructed from `ExternalContext` without input validation
+ public String parameterActionBad1() throws IOException {
+ FacesContext fc = FacesContext.getCurrentInstance();
+ Map params = fc.getExternalContext().getRequestParameterMap();
+ String loadUrl = params.get("loadUrl");
+
+ InputStreamReader isr = new InputStreamReader(fc.getExternalContext().getResourceAsStream(loadUrl));
+ BufferedReader br = new BufferedReader(isr);
+ if(br.ready()) {
+ //Do Stuff
+ return "result";
+ }
+
+ return "home";
+ }
+
+ // BAD: getResource constructed from `ExternalContext` without input validation
+ public String parameterActionBad2() throws IOException {
+ FacesContext fc = FacesContext.getCurrentInstance();
+ Map params = fc.getExternalContext().getRequestParameterMap();
+ String loadUrl = params.get("loadUrl");
+
+ URL url = fc.getExternalContext().getResource(loadUrl);
+
+ InputStream in = url.openStream();
+ //Do Stuff
+ return "result";
+ }
+
+ // GOOD: getResource constructed from `ExternalContext` with input validation
+ public String parameterActionGood1() throws IOException {
+ FacesContext fc = FacesContext.getCurrentInstance();
+ Map params = fc.getExternalContext().getRequestParameterMap();
+ String loadUrl = params.get("loadUrl");
+
+ if (loadUrl.equals("/public/crossdomain.xml")) {
+ URL url = fc.getExternalContext().getResource(loadUrl);
+
+ InputStream in = url.openStream();
+ //Do Stuff
+ return "result";
+ }
+
+ return "home";
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java b/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
new file mode 100644
index 000000000000..ee63939b209e
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
@@ -0,0 +1,131 @@
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+
+public class UnsafeServletRequestDispatch extends HttpServlet {
+ private static final String BASE_PATH = "/pages";
+
+ @Override
+ // BAD: Request dispatcher constructed from `ServletContext` without input validation
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String action = request.getParameter("action");
+ String returnURL = request.getParameter("returnURL");
+
+ ServletConfig cfg = getServletConfig();
+ if (action.equals("Login")) {
+ ServletContext sc = cfg.getServletContext();
+ RequestDispatcher rd = sc.getRequestDispatcher("/Login.jsp");
+ rd.forward(request, response);
+ } else {
+ ServletContext sc = cfg.getServletContext();
+ RequestDispatcher rd = sc.getRequestDispatcher(returnURL);
+ rd.forward(request, response);
+ }
+ }
+
+ @Override
+ // BAD: Request dispatcher constructed from `HttpServletRequest` without input validation
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String action = request.getParameter("action");
+ String returnURL = request.getParameter("returnURL");
+
+ if (action.equals("Login")) {
+ RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
+ rd.forward(request, response);
+ } else {
+ RequestDispatcher rd = request.getRequestDispatcher(returnURL);
+ rd.forward(request, response);
+ }
+ }
+
+ @Override
+ // GOOD: Request dispatcher with a whitelisted URI
+ protected void doPut(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String action = request.getParameter("action");
+
+ if (action.equals("Login")) {
+ RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
+ rd.forward(request, response);
+ } else if (action.equals("Register")) {
+ RequestDispatcher rd = request.getRequestDispatcher("/Register.jsp");
+ rd.forward(request, response);
+ }
+ }
+
+ // BAD: Request dispatcher without path traversal check
+ protected void doHead2(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+
+ // A sample payload "/pages/welcome.jsp/../WEB-INF/web.xml" can bypass the `startsWith` check
+ // The payload "/pages/welcome.jsp/../../%57EB-INF/web.xml" can bypass the check as well since RequestDispatcher will decode `%57` as `W`
+ if (path.startsWith(BASE_PATH)) {
+ request.getServletContext().getRequestDispatcher(path).include(request, response);
+ }
+ }
+
+ // GOOD: Request dispatcher with path traversal check
+ protected void doHead3(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+
+ if (path.startsWith(BASE_PATH) && !path.contains("..")) {
+ request.getServletContext().getRequestDispatcher(path).include(request, response);
+ }
+ }
+
+ // GOOD: Request dispatcher with path normalization and comparison
+ protected void doHead4(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+ Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
+
+ // /pages/welcome.jsp/../../WEB-INF/web.xml becomes /WEB-INF/web.xml
+ // /pages/welcome.jsp/../../%57EB-INF/web.xml becomes /%57EB-INF/web.xml
+ if (requestedPath.startsWith(BASE_PATH)) {
+ request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response);
+ }
+ }
+
+ // FN: Request dispatcher with negation check and path normalization, but without URL decoding
+ // When promoting this query, consider using FlowStates to make `getRequestDispatcher` a sink
+ // only if a URL-decoding step has NOT been crossed (i.e. make URLDecoder.decode change the
+ // state to a different value than the one required at the sink).
+ protected void doHead5(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+ Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
+
+ if (!requestedPath.startsWith("/WEB-INF") && !requestedPath.startsWith("/META-INF")) {
+ request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response);
+ }
+ }
+
+ // GOOD: Request dispatcher with path traversal check and URL decoding
+ protected void doHead6(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+ boolean hasEncoding = path.contains("%");
+ while (hasEncoding) {
+ path = URLDecoder.decode(path, "UTF-8");
+ hasEncoding = path.contains("%");
+ }
+
+ if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ request.getServletContext().getRequestDispatcher(path).include(request, response);
+ }
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.expected b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.expected
new file mode 100644
index 000000000000..5d809244fdba
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.expected
@@ -0,0 +1,129 @@
+edges
+| UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String |
+| UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource | UnsafeLoadSpringResource.java:35:31:35:33 | clr |
+| UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String | UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource |
+| UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | UnsafeLoadSpringResource.java:76:38:76:45 | fileName |
+| UnsafeLoadSpringResource.java:108:32:108:77 | fileName : String | UnsafeLoadSpringResource.java:116:51:116:58 | fileName |
+| UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | UnsafeRequestPath.java:23:33:23:36 | path |
+| UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:17:20:17:25 | params : Map |
+| UnsafeResourceGet2.java:17:20:17:25 | params : Map | UnsafeResourceGet2.java:17:20:17:40 | get(...) : String |
+| UnsafeResourceGet2.java:17:20:17:40 | get(...) : String | UnsafeResourceGet2.java:19:93:19:99 | loadUrl |
+| UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:33:20:33:25 | params : Map |
+| UnsafeResourceGet2.java:33:20:33:25 | params : Map | UnsafeResourceGet2.java:33:20:33:40 | get(...) : String |
+| UnsafeResourceGet2.java:33:20:33:40 | get(...) : String | UnsafeResourceGet2.java:35:49:35:55 | loadUrl : String |
+| UnsafeResourceGet2.java:35:13:35:56 | getResource(...) : URL | UnsafeResourceGet2.java:37:20:37:22 | url |
+| UnsafeResourceGet2.java:35:49:35:55 | loadUrl : String | UnsafeResourceGet2.java:35:13:35:56 | getResource(...) : URL |
+| UnsafeResourceGet.java:32:23:32:56 | getParameter(...) : String | UnsafeResourceGet.java:39:28:39:37 | requestUrl : String |
+| UnsafeResourceGet.java:39:13:39:38 | getResource(...) : URL | UnsafeResourceGet.java:41:20:41:22 | url |
+| UnsafeResourceGet.java:39:28:39:37 | requestUrl : String | UnsafeResourceGet.java:39:13:39:38 | getResource(...) : URL |
+| UnsafeResourceGet.java:111:24:111:58 | getParameter(...) : String | UnsafeResourceGet.java:115:68:115:78 | requestPath |
+| UnsafeResourceGet.java:143:23:143:56 | getParameter(...) : String | UnsafeResourceGet.java:148:36:148:45 | requestUrl : String |
+| UnsafeResourceGet.java:148:13:148:46 | getResource(...) : URL | UnsafeResourceGet.java:150:20:150:22 | url |
+| UnsafeResourceGet.java:148:36:148:45 | requestUrl : String | UnsafeResourceGet.java:148:13:148:46 | getResource(...) : URL |
+| UnsafeResourceGet.java:181:24:181:58 | getParameter(...) : String | UnsafeResourceGet.java:189:68:189:78 | requestPath |
+| UnsafeResourceGet.java:219:23:219:56 | getParameter(...) : String | UnsafeResourceGet.java:224:53:224:62 | requestUrl : String |
+| UnsafeResourceGet.java:224:13:224:63 | getResource(...) : URL | UnsafeResourceGet.java:226:20:226:22 | url |
+| UnsafeResourceGet.java:224:53:224:62 | requestUrl : String | UnsafeResourceGet.java:224:13:224:63 | getResource(...) : URL |
+| UnsafeResourceGet.java:237:24:237:58 | getParameter(...) : String | UnsafeResourceGet.java:241:33:241:43 | requestPath : String |
+| UnsafeResourceGet.java:241:18:241:44 | getResource(...) : Resource | UnsafeResourceGet.java:245:21:245:22 | rs : Resource |
+| UnsafeResourceGet.java:241:33:241:43 | requestPath : String | UnsafeResourceGet.java:241:18:241:44 | getResource(...) : Resource |
+| UnsafeResourceGet.java:245:21:245:22 | rs : Resource | UnsafeResourceGet.java:245:21:245:32 | getPath(...) |
+| UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL |
+| UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL |
+| UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | UnsafeServletRequestDispatch.java:76:53:76:56 | path |
+| UnsafeUrlForward.java:13:27:13:36 | url : String | UnsafeUrlForward.java:14:27:14:29 | url |
+| UnsafeUrlForward.java:18:27:18:36 | url : String | UnsafeUrlForward.java:20:28:20:30 | url |
+| UnsafeUrlForward.java:25:21:25:30 | url : String | UnsafeUrlForward.java:26:23:26:25 | url |
+| UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:48:31:63 | ... + ... |
+| UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:61:31:63 | url |
+| UnsafeUrlForward.java:36:19:36:28 | url : String | UnsafeUrlForward.java:38:33:38:35 | url |
+| UnsafeUrlForward.java:47:19:47:28 | url : String | UnsafeUrlForward.java:49:33:49:62 | ... + ... |
+| UnsafeUrlForward.java:58:19:58:28 | url : String | UnsafeUrlForward.java:60:33:60:62 | ... + ... |
+nodes
+| UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | semmle.label | fileName : String |
+| UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource | semmle.label | new ClassPathResource(...) : ClassPathResource |
+| UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String | semmle.label | fileName : String |
+| UnsafeLoadSpringResource.java:35:31:35:33 | clr | semmle.label | clr |
+| UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | semmle.label | fileName : String |
+| UnsafeLoadSpringResource.java:76:38:76:45 | fileName | semmle.label | fileName |
+| UnsafeLoadSpringResource.java:108:32:108:77 | fileName : String | semmle.label | fileName : String |
+| UnsafeLoadSpringResource.java:116:51:116:58 | fileName | semmle.label | fileName |
+| UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | semmle.label | getServletPath(...) : String |
+| UnsafeRequestPath.java:23:33:23:36 | path | semmle.label | path |
+| UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | semmle.label | getRequestParameterMap(...) : Map |
+| UnsafeResourceGet2.java:17:20:17:25 | params : Map | semmle.label | params : Map |
+| UnsafeResourceGet2.java:17:20:17:40 | get(...) : String | semmle.label | get(...) : String |
+| UnsafeResourceGet2.java:19:93:19:99 | loadUrl | semmle.label | loadUrl |
+| UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) : Map | semmle.label | getRequestParameterMap(...) : Map |
+| UnsafeResourceGet2.java:33:20:33:25 | params : Map | semmle.label | params : Map |
+| UnsafeResourceGet2.java:33:20:33:40 | get(...) : String | semmle.label | get(...) : String |
+| UnsafeResourceGet2.java:35:13:35:56 | getResource(...) : URL | semmle.label | getResource(...) : URL |
+| UnsafeResourceGet2.java:35:49:35:55 | loadUrl : String | semmle.label | loadUrl : String |
+| UnsafeResourceGet2.java:37:20:37:22 | url | semmle.label | url |
+| UnsafeResourceGet.java:32:23:32:56 | getParameter(...) : String | semmle.label | getParameter(...) : String |
+| UnsafeResourceGet.java:39:13:39:38 | getResource(...) : URL | semmle.label | getResource(...) : URL |
+| UnsafeResourceGet.java:39:28:39:37 | requestUrl : String | semmle.label | requestUrl : String |
+| UnsafeResourceGet.java:41:20:41:22 | url | semmle.label | url |
+| UnsafeResourceGet.java:111:24:111:58 | getParameter(...) : String | semmle.label | getParameter(...) : String |
+| UnsafeResourceGet.java:115:68:115:78 | requestPath | semmle.label | requestPath |
+| UnsafeResourceGet.java:143:23:143:56 | getParameter(...) : String | semmle.label | getParameter(...) : String |
+| UnsafeResourceGet.java:148:13:148:46 | getResource(...) : URL | semmle.label | getResource(...) : URL |
+| UnsafeResourceGet.java:148:36:148:45 | requestUrl : String | semmle.label | requestUrl : String |
+| UnsafeResourceGet.java:150:20:150:22 | url | semmle.label | url |
+| UnsafeResourceGet.java:181:24:181:58 | getParameter(...) : String | semmle.label | getParameter(...) : String |
+| UnsafeResourceGet.java:189:68:189:78 | requestPath | semmle.label | requestPath |
+| UnsafeResourceGet.java:219:23:219:56 | getParameter(...) : String | semmle.label | getParameter(...) : String |
+| UnsafeResourceGet.java:224:13:224:63 | getResource(...) : URL | semmle.label | getResource(...) : URL |
+| UnsafeResourceGet.java:224:53:224:62 | requestUrl : String | semmle.label | requestUrl : String |
+| UnsafeResourceGet.java:226:20:226:22 | url | semmle.label | url |
+| UnsafeResourceGet.java:237:24:237:58 | getParameter(...) : String | semmle.label | getParameter(...) : String |
+| UnsafeResourceGet.java:241:18:241:44 | getResource(...) : Resource | semmle.label | getResource(...) : Resource |
+| UnsafeResourceGet.java:241:33:241:43 | requestPath : String | semmle.label | requestPath : String |
+| UnsafeResourceGet.java:245:21:245:22 | rs : Resource | semmle.label | rs : Resource |
+| UnsafeResourceGet.java:245:21:245:32 | getPath(...) | semmle.label | getPath(...) |
+| UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | semmle.label | getParameter(...) : String |
+| UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | semmle.label | returnURL |
+| UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | semmle.label | getParameter(...) : String |
+| UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | semmle.label | returnURL |
+| UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | semmle.label | getParameter(...) : String |
+| UnsafeServletRequestDispatch.java:76:53:76:56 | path | semmle.label | path |
+| UnsafeUrlForward.java:13:27:13:36 | url : String | semmle.label | url : String |
+| UnsafeUrlForward.java:14:27:14:29 | url | semmle.label | url |
+| UnsafeUrlForward.java:18:27:18:36 | url : String | semmle.label | url : String |
+| UnsafeUrlForward.java:20:28:20:30 | url | semmle.label | url |
+| UnsafeUrlForward.java:25:21:25:30 | url : String | semmle.label | url : String |
+| UnsafeUrlForward.java:26:23:26:25 | url | semmle.label | url |
+| UnsafeUrlForward.java:30:27:30:36 | url : String | semmle.label | url : String |
+| UnsafeUrlForward.java:31:48:31:63 | ... + ... | semmle.label | ... + ... |
+| UnsafeUrlForward.java:31:61:31:63 | url | semmle.label | url |
+| UnsafeUrlForward.java:36:19:36:28 | url : String | semmle.label | url : String |
+| UnsafeUrlForward.java:38:33:38:35 | url | semmle.label | url |
+| UnsafeUrlForward.java:47:19:47:28 | url : String | semmle.label | url : String |
+| UnsafeUrlForward.java:49:33:49:62 | ... + ... | semmle.label | ... + ... |
+| UnsafeUrlForward.java:58:19:58:28 | url : String | semmle.label | url : String |
+| UnsafeUrlForward.java:60:33:60:62 | ... + ... | semmle.label | ... + ... |
+subpaths
+#select
+| UnsafeLoadSpringResource.java:35:31:35:33 | clr | UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | UnsafeLoadSpringResource.java:35:31:35:33 | clr | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:27:32:27:77 | fileName | user-provided value |
+| UnsafeLoadSpringResource.java:76:38:76:45 | fileName | UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | UnsafeLoadSpringResource.java:76:38:76:45 | fileName | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:68:32:68:77 | fileName | user-provided value |
+| UnsafeLoadSpringResource.java:116:51:116:58 | fileName | UnsafeLoadSpringResource.java:108:32:108:77 | fileName : String | UnsafeLoadSpringResource.java:116:51:116:58 | fileName | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:108:32:108:77 | fileName | user-provided value |
+| UnsafeRequestPath.java:23:33:23:36 | path | UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | UnsafeRequestPath.java:23:33:23:36 | path | Potentially untrusted URL forward due to $@. | UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) | user-provided value |
+| UnsafeResourceGet2.java:19:93:19:99 | loadUrl | UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:19:93:19:99 | loadUrl | Potentially untrusted URL forward due to $@. | UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) | user-provided value |
+| UnsafeResourceGet2.java:37:20:37:22 | url | UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:37:20:37:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) | user-provided value |
+| UnsafeResourceGet.java:41:20:41:22 | url | UnsafeResourceGet.java:32:23:32:56 | getParameter(...) : String | UnsafeResourceGet.java:41:20:41:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:32:23:32:56 | getParameter(...) | user-provided value |
+| UnsafeResourceGet.java:115:68:115:78 | requestPath | UnsafeResourceGet.java:111:24:111:58 | getParameter(...) : String | UnsafeResourceGet.java:115:68:115:78 | requestPath | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:111:24:111:58 | getParameter(...) | user-provided value |
+| UnsafeResourceGet.java:150:20:150:22 | url | UnsafeResourceGet.java:143:23:143:56 | getParameter(...) : String | UnsafeResourceGet.java:150:20:150:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:143:23:143:56 | getParameter(...) | user-provided value |
+| UnsafeResourceGet.java:189:68:189:78 | requestPath | UnsafeResourceGet.java:181:24:181:58 | getParameter(...) : String | UnsafeResourceGet.java:189:68:189:78 | requestPath | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:181:24:181:58 | getParameter(...) | user-provided value |
+| UnsafeResourceGet.java:226:20:226:22 | url | UnsafeResourceGet.java:219:23:219:56 | getParameter(...) : String | UnsafeResourceGet.java:226:20:226:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:219:23:219:56 | getParameter(...) | user-provided value |
+| UnsafeResourceGet.java:245:21:245:32 | getPath(...) | UnsafeResourceGet.java:237:24:237:58 | getParameter(...) : String | UnsafeResourceGet.java:245:21:245:32 | getPath(...) | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:237:24:237:58 | getParameter(...) | user-provided value |
+| UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) | user-provided value |
+| UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) | user-provided value |
+| UnsafeServletRequestDispatch.java:76:53:76:56 | path | UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | UnsafeServletRequestDispatch.java:76:53:76:56 | path | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) | user-provided value |
+| UnsafeUrlForward.java:14:27:14:29 | url | UnsafeUrlForward.java:13:27:13:36 | url : String | UnsafeUrlForward.java:14:27:14:29 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:13:27:13:36 | url | user-provided value |
+| UnsafeUrlForward.java:20:28:20:30 | url | UnsafeUrlForward.java:18:27:18:36 | url : String | UnsafeUrlForward.java:20:28:20:30 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:18:27:18:36 | url | user-provided value |
+| UnsafeUrlForward.java:26:23:26:25 | url | UnsafeUrlForward.java:25:21:25:30 | url : String | UnsafeUrlForward.java:26:23:26:25 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:25:21:25:30 | url | user-provided value |
+| UnsafeUrlForward.java:31:48:31:63 | ... + ... | UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:48:31:63 | ... + ... | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:30:27:30:36 | url | user-provided value |
+| UnsafeUrlForward.java:31:61:31:63 | url | UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:61:31:63 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:30:27:30:36 | url | user-provided value |
+| UnsafeUrlForward.java:38:33:38:35 | url | UnsafeUrlForward.java:36:19:36:28 | url : String | UnsafeUrlForward.java:38:33:38:35 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:36:19:36:28 | url | user-provided value |
+| UnsafeUrlForward.java:49:33:49:62 | ... + ... | UnsafeUrlForward.java:47:19:47:28 | url : String | UnsafeUrlForward.java:49:33:49:62 | ... + ... | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:47:19:47:28 | url | user-provided value |
+| UnsafeUrlForward.java:60:33:60:62 | ... + ... | UnsafeUrlForward.java:58:19:58:28 | url : String | UnsafeUrlForward.java:60:33:60:62 | ... + ... | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:58:19:58:28 | url | user-provided value |
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java
new file mode 100644
index 000000000000..4018ed289481
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java
@@ -0,0 +1,78 @@
+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 ModelAndView bad2(String url) {
+ ModelAndView modelAndView = new ModelAndView();
+ modelAndView.setViewName(url);
+ return modelAndView;
+ }
+
+ @GetMapping("/bad3")
+ public String bad3(String url) {
+ return "forward:" + url + "/swagger-ui/index.html";
+ }
+
+ @GetMapping("/bad4")
+ public ModelAndView bad4(String url) {
+ ModelAndView modelAndView = new ModelAndView("forward:" + url);
+ return modelAndView;
+ }
+
+ @GetMapping("/bad5")
+ public void bad5(String url, HttpServletRequest request, HttpServletResponse response) {
+ try {
+ request.getRequestDispatcher(url).include(request, response);
+ } catch (ServletException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @GetMapping("/bad6")
+ public void bad6(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("/bad7")
+ public void bad7(String url, HttpServletRequest request, HttpServletResponse response) {
+ try {
+ request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").forward(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();
+ }
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.qlref b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.qlref
new file mode 100644
index 000000000000..934a18cc6c78
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.qlref
@@ -0,0 +1 @@
+experimental/Security/CWE/CWE-552/UnsafeUrlForward.ql
diff --git a/java/ql/test/query-tests/security/CWE-552/options b/java/ql/test/query-tests/security/CWE-552/options
new file mode 100644
index 000000000000..025b888db025
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/options
@@ -0,0 +1 @@
+//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/springframework-5.3.8/:${testdir}/../../../stubs/javax-faces-2.3/:${testdir}/../../../stubs/undertow-io-2.2/:${testdir}/../../../stubs/jboss-vfs-3.2/:${testdir}/../../../stubs/springframework-5.3.8/
From 2793f28428567060b66819525a5a045f52f2228d Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 20 Nov 2023 16:56:41 -0500
Subject: [PATCH 02/40] Java: move config to Query.qll file
---
java/ql/lib/semmle/code/java/Jsf.qll | 35 +++++++++++++++
.../lib/semmle/code/java/SpringResource.qll | 22 +++++++++
.../code/java/security}/UnsafeUrlForward.qll | 4 +-
.../java/security/UnsafeUrlForwardQuery.qll | 45 +++++++++++++++++++
.../Security/CWE/CWE-552/UnsafeUrlForward.ql | 45 +------------------
5 files changed, 105 insertions(+), 46 deletions(-)
create mode 100644 java/ql/lib/semmle/code/java/Jsf.qll
create mode 100644 java/ql/lib/semmle/code/java/SpringResource.qll
rename java/ql/{src/Security/CWE/CWE-552 => lib/semmle/code/java/security}/UnsafeUrlForward.qll (97%)
create mode 100644 java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
diff --git a/java/ql/lib/semmle/code/java/Jsf.qll b/java/ql/lib/semmle/code/java/Jsf.qll
new file mode 100644
index 000000000000..9023953add4b
--- /dev/null
+++ b/java/ql/lib/semmle/code/java/Jsf.qll
@@ -0,0 +1,35 @@
+/**
+ * Provides classes and predicates for working with the Java Server Faces (JSF).
+ */
+
+// TODO: COMBINE WITH EXISTING JSF-RELATED QLL FILES!
+import java
+
+/**
+ * The JSF class `ExternalContext` for processing HTTP requests.
+ */
+class ExternalContext extends RefType {
+ ExternalContext() {
+ this.hasQualifiedName(["javax.faces.context", "jakarta.faces.context"], "ExternalContext")
+ }
+}
+
+/**
+ * The method `getResource()` declared in JSF `ExternalContext`.
+ */
+class GetFacesResourceMethod extends Method {
+ GetFacesResourceMethod() {
+ this.getDeclaringType().getASupertype*() instanceof ExternalContext and
+ this.hasName("getResource")
+ }
+}
+
+/**
+ * The method `getResourceAsStream()` declared in JSF `ExternalContext`.
+ */
+class GetFacesResourceAsStreamMethod extends Method {
+ GetFacesResourceAsStreamMethod() {
+ this.getDeclaringType().getASupertype*() instanceof ExternalContext and
+ this.hasName("getResourceAsStream")
+ }
+}
diff --git a/java/ql/lib/semmle/code/java/SpringResource.qll b/java/ql/lib/semmle/code/java/SpringResource.qll
new file mode 100644
index 000000000000..a7d3a3793b6d
--- /dev/null
+++ b/java/ql/lib/semmle/code/java/SpringResource.qll
@@ -0,0 +1,22 @@
+/**
+ * Provides classes for working with resource loading in Spring.
+ */
+
+// TODO: COMBINE WITH EXISTING SPRING-RELATED QLL FILES!
+import java
+private import semmle.code.java.dataflow.FlowSources
+
+/** A utility class for resolving resource locations to files in the file system in the Spring framework. */
+class ResourceUtils extends Class {
+ ResourceUtils() { this.hasQualifiedName("org.springframework.util", "ResourceUtils") }
+}
+
+/**
+ * A method declared in `org.springframework.util.ResourceUtils` that loads Spring resources.
+ */
+class GetResourceUtilsMethod extends Method {
+ GetResourceUtilsMethod() {
+ this.getDeclaringType().getASupertype*() instanceof ResourceUtils and
+ this.hasName(["extractArchiveURL", "extractJarFileURL", "getFile", "getURL"])
+ }
+}
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
similarity index 97%
rename from java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qll
rename to java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
index db610eb65cec..48b4431015e4 100644
--- a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
@@ -1,10 +1,10 @@
import java
-private import experimental.semmle.code.java.frameworks.Jsf
+private import semmle.code.java.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.SpringResource
/** A sink for unsafe URL forward vulnerabilities. */
abstract class UnsafeUrlForwardSink extends DataFlow::Node { }
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
new file mode 100644
index 000000000000..9ee3f2ab4170
--- /dev/null
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
@@ -0,0 +1,45 @@
+import java
+import semmle.code.java.security.UnsafeUrlForward
+import semmle.code.java.dataflow.FlowSources
+import semmle.code.java.dataflow.TaintTracking
+import semmle.code.java.Jsf
+import semmle.code.java.security.PathSanitizer
+
+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;
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
index 240023f9ffc0..4e3326a831ee 100644
--- a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
+++ b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
@@ -11,52 +11,9 @@
*/
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 semmle.code.java.security.UnsafeUrlForwardQuery
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;
-
from UnsafeUrlForwardFlow::PathNode source, UnsafeUrlForwardFlow::PathNode sink
where UnsafeUrlForwardFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Potentially untrusted URL forward due to $@.",
From 35a083ae9e5c70a29ac426cf54112ef98286507b Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 20 Nov 2023 16:59:19 -0500
Subject: [PATCH 03/40] Java: update test cases to use inline expectations
---
.../CWE-552/UnsafeLoadSpringResource.java | 10 +-
.../security/CWE-552/UnsafeRequestPath.java | 12 +-
.../security/CWE-552/UnsafeResourceGet.java | 12 +-
.../security/CWE-552/UnsafeResourceGet2.java | 4 +-
.../CWE-552/UnsafeServletRequestDispatch.java | 20 +--
.../CWE-552/UnsafeUrlForward.expected | 129 ------------------
.../security/CWE-552/UnsafeUrlForward.java | 14 +-
.../security/CWE-552/UnsafeUrlForward.qlref | 1 -
.../CWE-552/UnsafeUrlForwardTest.expected | 2 +
.../security/CWE-552/UnsafeUrlForwardTest.ql | 18 +++
10 files changed, 56 insertions(+), 166 deletions(-)
delete mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.expected
delete mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.qlref
create mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.expected
create mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.ql
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java b/java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
index c7e114aede35..363d84cabe90 100644
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
@@ -32,7 +32,7 @@ public String getFileContent1(@RequestParam(name="fileName") String fileName) {
char[] buffer = new char[4096];
StringBuilder out = new StringBuilder();
try {
- Reader in = new FileReader(clr.getFilename());
+ Reader in = new FileReader(clr.getFilename()); // $ hasUnsafeUrlForward (path-inj?)
for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
out.append(buffer, 0, numRead);
}
@@ -67,13 +67,13 @@ public String getFileContent1a(@RequestParam(name="fileName") String fileName) {
//BAD: Get resource from ResourceUtils without input validation
public String getFileContent2(@RequestParam(name="fileName") String fileName) {
String content = null;
-
+
try {
// A request such as the following can disclose source code and system configuration
// fileName=/etc/hosts
// fileName=file:/etc/hosts
// fileName=/opt/appdir/WEB-INF/views/page.jsp
- File file = ResourceUtils.getFile(fileName);
+ File file = ResourceUtils.getFile(fileName); // $ hasUnsafeUrlForward (path-inj?)
//Read File Content
content = new String(Files.readAllBytes(file.toPath()));
} catch (IOException ie) {
@@ -86,7 +86,7 @@ public String getFileContent2(@RequestParam(name="fileName") String fileName) {
//GOOD: Get resource from ResourceUtils with input path validation
public String getFileContent2a(@RequestParam(name="fileName") String fileName) {
String content = null;
-
+
if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
try {
File file = ResourceUtils.getFile(fileName);
@@ -113,7 +113,7 @@ public String getFileContent3(@RequestParam(name="fileName") String fileName) {
// fileName=/WEB-INF/views/page.jsp
// fileName=/WEB-INF/classes/com/example/package/SampleController.class
// fileName=file:/etc/hosts
- Resource resource = resourceLoader.getResource(fileName);
+ Resource resource = resourceLoader.getResource(fileName); // $ hasUnsafeUrlForward (path-inj?)
char[] buffer = new char[4096];
StringBuilder out = new StringBuilder();
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java b/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
index 2de0cae0d3c5..55afe84bc192 100644
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
@@ -14,23 +14,23 @@ public class UnsafeRequestPath implements Filter {
private static final String BASE_PATH = "/pages";
@Override
- // BAD: Request dispatcher from servlet path without check
+ // BAD: Request dispatcher from servlet path without check
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String path = ((HttpServletRequest) request).getServletPath();
// A sample payload "/%57EB-INF/web.xml" can bypass this `startsWith` check
if (path != null && !path.startsWith("/WEB-INF")) {
- request.getRequestDispatcher(path).forward(request, response);
+ request.getRequestDispatcher(path).forward(request, response); // $ hasUnsafeUrlForward
} else {
chain.doFilter(request, response);
}
}
- // GOOD: Request dispatcher from servlet path with check
+ // GOOD: Request dispatcher from servlet path with check
public void doFilter2(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String path = ((HttpServletRequest) request).getServletPath();
-
+
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
request.getRequestDispatcher(path).forward(request, response);
} else {
@@ -38,11 +38,11 @@ public void doFilter2(ServletRequest request, ServletResponse response, FilterCh
}
}
- // GOOD: Request dispatcher from servlet path with whitelisted string comparison
+ // GOOD: Request dispatcher from servlet path with whitelisted string comparison
public void doFilter3(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String path = ((HttpServletRequest) request).getServletPath();
-
+
if (path.equals("/comaction")) {
request.getRequestDispatcher(path).forward(request, response);
} else {
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java b/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java
index 64c23334f187..053887984c61 100644
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java
@@ -38,7 +38,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
// A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
URL url = sc.getResource(requestUrl);
- InputStream in = url.openStream();
+ InputStream in = url.openStream(); // $ hasUnsafeUrlForward (SSRF)
byte[] buf = new byte[4 * 1024]; // 4K buffer
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
@@ -112,7 +112,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
ServletOutputStream out = response.getOutputStream();
// A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
- InputStream in = request.getServletContext().getResourceAsStream(requestPath);
+ InputStream in = request.getServletContext().getResourceAsStream(requestPath); // $ hasUnsafeUrlForward (path-inj?)
byte[] buf = new byte[4 * 1024]; // 4K buffer
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
@@ -147,7 +147,7 @@ protected void doHead(HttpServletRequest request, HttpServletResponse response)
// Note the class is in two levels of subpackages and `Class.getResource` starts from its own directory
URL url = getClass().getResource(requestUrl);
- InputStream in = url.openStream();
+ InputStream in = url.openStream(); // $ hasUnsafeUrlForward (SSRF)
byte[] buf = new byte[4 * 1024]; // 4K buffer
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
@@ -186,7 +186,7 @@ protected void doPut(HttpServletRequest request, HttpServletResponse response)
// A sample request /fake.jsp/../../../WEB-INF/web.xml can load the web.xml file
// Note the class is in two levels of subpackages and `ClassLoader.getResourceAsStream` starts from its own directory
- InputStream in = getClass().getClassLoader().getResourceAsStream(requestPath);
+ InputStream in = getClass().getClassLoader().getResourceAsStream(requestPath); // $ hasUnsafeUrlForward (path-inj?)
byte[] buf = new byte[4 * 1024]; // 4K buffer
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
@@ -223,7 +223,7 @@ protected void doPutBad(HttpServletRequest request, HttpServletResponse response
// Note the class is in two levels of subpackages and `ClassLoader.getResource` starts from its own directory
URL url = getClass().getClassLoader().getResource(requestUrl);
- InputStream in = url.openStream();
+ InputStream in = url.openStream(); // $ hasUnsafeUrlForward (SSRF)
byte[] buf = new byte[4 * 1024]; // 4K buffer
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
@@ -242,7 +242,7 @@ protected void doPutBad2(HttpServletRequest request, HttpServletResponse respons
VirtualFile overlay = VFS.getChild(new URI("EAP_HOME/modules/"));
// Do file operations
- overlay.getChild(rs.getPath());
+ overlay.getChild(rs.getPath()); // $ hasUnsafeUrlForward (path-inj?)
} catch (URISyntaxException ue) {
throw new IOException("Cannot parse the URI");
}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java b/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java
index b3d041d024cf..0043bb06c67a 100644
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java
@@ -16,7 +16,7 @@ public String parameterActionBad1() throws IOException {
Map params = fc.getExternalContext().getRequestParameterMap();
String loadUrl = params.get("loadUrl");
- InputStreamReader isr = new InputStreamReader(fc.getExternalContext().getResourceAsStream(loadUrl));
+ InputStreamReader isr = new InputStreamReader(fc.getExternalContext().getResourceAsStream(loadUrl)); // $ hasUnsafeUrlForward (path-inj?)
BufferedReader br = new BufferedReader(isr);
if(br.ready()) {
//Do Stuff
@@ -34,7 +34,7 @@ public String parameterActionBad2() throws IOException {
URL url = fc.getExternalContext().getResource(loadUrl);
- InputStream in = url.openStream();
+ InputStream in = url.openStream(); // $ hasUnsafeUrlForward (SSRF)
//Do Stuff
return "result";
}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java b/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
index ee63939b209e..9d501f2ec0df 100644
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
@@ -29,13 +29,13 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
rd.forward(request, response);
} else {
ServletContext sc = cfg.getServletContext();
- RequestDispatcher rd = sc.getRequestDispatcher(returnURL);
+ RequestDispatcher rd = sc.getRequestDispatcher(returnURL); // $ hasUnsafeUrlForward
rd.forward(request, response);
}
}
@Override
- // BAD: Request dispatcher constructed from `HttpServletRequest` without input validation
+ // BAD: Request dispatcher constructed from `HttpServletRequest` without input validation
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String action = request.getParameter("action");
@@ -45,7 +45,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
rd.forward(request, response);
} else {
- RequestDispatcher rd = request.getRequestDispatcher(returnURL);
+ RequestDispatcher rd = request.getRequestDispatcher(returnURL); // $ hasUnsafeUrlForward
rd.forward(request, response);
}
}
@@ -65,22 +65,22 @@ protected void doPut(HttpServletRequest request, HttpServletResponse response)
}
}
- // BAD: Request dispatcher without path traversal check
+ // BAD: Request dispatcher without path traversal check
protected void doHead2(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
- // A sample payload "/pages/welcome.jsp/../WEB-INF/web.xml" can bypass the `startsWith` check
- // The payload "/pages/welcome.jsp/../../%57EB-INF/web.xml" can bypass the check as well since RequestDispatcher will decode `%57` as `W`
+ // A sample payload "/pages/welcome.jsp/../WEB-INF/web.xml" can bypass the `startsWith` check
+ // The payload "/pages/welcome.jsp/../../%57EB-INF/web.xml" can bypass the check as well since RequestDispatcher will decode `%57` as `W`
if (path.startsWith(BASE_PATH)) {
- request.getServletContext().getRequestDispatcher(path).include(request, response);
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUnsafeUrlForward
}
}
- // GOOD: Request dispatcher with path traversal check
+ // GOOD: Request dispatcher with path traversal check
protected void doHead3(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- String path = request.getParameter("path");
+ String path = request.getParameter("path");
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
request.getServletContext().getRequestDispatcher(path).include(request, response);
@@ -110,7 +110,7 @@ protected void doHead5(HttpServletRequest request, HttpServletResponse response)
Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
if (!requestedPath.startsWith("/WEB-INF") && !requestedPath.startsWith("/META-INF")) {
- request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response);
+ request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ MISSING: hasUnsafeUrlForward
}
}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.expected b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.expected
deleted file mode 100644
index 5d809244fdba..000000000000
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.expected
+++ /dev/null
@@ -1,129 +0,0 @@
-edges
-| UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String |
-| UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource | UnsafeLoadSpringResource.java:35:31:35:33 | clr |
-| UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String | UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource |
-| UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | UnsafeLoadSpringResource.java:76:38:76:45 | fileName |
-| UnsafeLoadSpringResource.java:108:32:108:77 | fileName : String | UnsafeLoadSpringResource.java:116:51:116:58 | fileName |
-| UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | UnsafeRequestPath.java:23:33:23:36 | path |
-| UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:17:20:17:25 | params : Map |
-| UnsafeResourceGet2.java:17:20:17:25 | params : Map | UnsafeResourceGet2.java:17:20:17:40 | get(...) : String |
-| UnsafeResourceGet2.java:17:20:17:40 | get(...) : String | UnsafeResourceGet2.java:19:93:19:99 | loadUrl |
-| UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:33:20:33:25 | params : Map |
-| UnsafeResourceGet2.java:33:20:33:25 | params : Map | UnsafeResourceGet2.java:33:20:33:40 | get(...) : String |
-| UnsafeResourceGet2.java:33:20:33:40 | get(...) : String | UnsafeResourceGet2.java:35:49:35:55 | loadUrl : String |
-| UnsafeResourceGet2.java:35:13:35:56 | getResource(...) : URL | UnsafeResourceGet2.java:37:20:37:22 | url |
-| UnsafeResourceGet2.java:35:49:35:55 | loadUrl : String | UnsafeResourceGet2.java:35:13:35:56 | getResource(...) : URL |
-| UnsafeResourceGet.java:32:23:32:56 | getParameter(...) : String | UnsafeResourceGet.java:39:28:39:37 | requestUrl : String |
-| UnsafeResourceGet.java:39:13:39:38 | getResource(...) : URL | UnsafeResourceGet.java:41:20:41:22 | url |
-| UnsafeResourceGet.java:39:28:39:37 | requestUrl : String | UnsafeResourceGet.java:39:13:39:38 | getResource(...) : URL |
-| UnsafeResourceGet.java:111:24:111:58 | getParameter(...) : String | UnsafeResourceGet.java:115:68:115:78 | requestPath |
-| UnsafeResourceGet.java:143:23:143:56 | getParameter(...) : String | UnsafeResourceGet.java:148:36:148:45 | requestUrl : String |
-| UnsafeResourceGet.java:148:13:148:46 | getResource(...) : URL | UnsafeResourceGet.java:150:20:150:22 | url |
-| UnsafeResourceGet.java:148:36:148:45 | requestUrl : String | UnsafeResourceGet.java:148:13:148:46 | getResource(...) : URL |
-| UnsafeResourceGet.java:181:24:181:58 | getParameter(...) : String | UnsafeResourceGet.java:189:68:189:78 | requestPath |
-| UnsafeResourceGet.java:219:23:219:56 | getParameter(...) : String | UnsafeResourceGet.java:224:53:224:62 | requestUrl : String |
-| UnsafeResourceGet.java:224:13:224:63 | getResource(...) : URL | UnsafeResourceGet.java:226:20:226:22 | url |
-| UnsafeResourceGet.java:224:53:224:62 | requestUrl : String | UnsafeResourceGet.java:224:13:224:63 | getResource(...) : URL |
-| UnsafeResourceGet.java:237:24:237:58 | getParameter(...) : String | UnsafeResourceGet.java:241:33:241:43 | requestPath : String |
-| UnsafeResourceGet.java:241:18:241:44 | getResource(...) : Resource | UnsafeResourceGet.java:245:21:245:22 | rs : Resource |
-| UnsafeResourceGet.java:241:33:241:43 | requestPath : String | UnsafeResourceGet.java:241:18:241:44 | getResource(...) : Resource |
-| UnsafeResourceGet.java:245:21:245:22 | rs : Resource | UnsafeResourceGet.java:245:21:245:32 | getPath(...) |
-| UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL |
-| UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL |
-| UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | UnsafeServletRequestDispatch.java:76:53:76:56 | path |
-| UnsafeUrlForward.java:13:27:13:36 | url : String | UnsafeUrlForward.java:14:27:14:29 | url |
-| UnsafeUrlForward.java:18:27:18:36 | url : String | UnsafeUrlForward.java:20:28:20:30 | url |
-| UnsafeUrlForward.java:25:21:25:30 | url : String | UnsafeUrlForward.java:26:23:26:25 | url |
-| UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:48:31:63 | ... + ... |
-| UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:61:31:63 | url |
-| UnsafeUrlForward.java:36:19:36:28 | url : String | UnsafeUrlForward.java:38:33:38:35 | url |
-| UnsafeUrlForward.java:47:19:47:28 | url : String | UnsafeUrlForward.java:49:33:49:62 | ... + ... |
-| UnsafeUrlForward.java:58:19:58:28 | url : String | UnsafeUrlForward.java:60:33:60:62 | ... + ... |
-nodes
-| UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | semmle.label | fileName : String |
-| UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource | semmle.label | new ClassPathResource(...) : ClassPathResource |
-| UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String | semmle.label | fileName : String |
-| UnsafeLoadSpringResource.java:35:31:35:33 | clr | semmle.label | clr |
-| UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | semmle.label | fileName : String |
-| UnsafeLoadSpringResource.java:76:38:76:45 | fileName | semmle.label | fileName |
-| UnsafeLoadSpringResource.java:108:32:108:77 | fileName : String | semmle.label | fileName : String |
-| UnsafeLoadSpringResource.java:116:51:116:58 | fileName | semmle.label | fileName |
-| UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | semmle.label | getServletPath(...) : String |
-| UnsafeRequestPath.java:23:33:23:36 | path | semmle.label | path |
-| UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | semmle.label | getRequestParameterMap(...) : Map |
-| UnsafeResourceGet2.java:17:20:17:25 | params : Map | semmle.label | params : Map |
-| UnsafeResourceGet2.java:17:20:17:40 | get(...) : String | semmle.label | get(...) : String |
-| UnsafeResourceGet2.java:19:93:19:99 | loadUrl | semmle.label | loadUrl |
-| UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) : Map | semmle.label | getRequestParameterMap(...) : Map |
-| UnsafeResourceGet2.java:33:20:33:25 | params : Map | semmle.label | params : Map |
-| UnsafeResourceGet2.java:33:20:33:40 | get(...) : String | semmle.label | get(...) : String |
-| UnsafeResourceGet2.java:35:13:35:56 | getResource(...) : URL | semmle.label | getResource(...) : URL |
-| UnsafeResourceGet2.java:35:49:35:55 | loadUrl : String | semmle.label | loadUrl : String |
-| UnsafeResourceGet2.java:37:20:37:22 | url | semmle.label | url |
-| UnsafeResourceGet.java:32:23:32:56 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:39:13:39:38 | getResource(...) : URL | semmle.label | getResource(...) : URL |
-| UnsafeResourceGet.java:39:28:39:37 | requestUrl : String | semmle.label | requestUrl : String |
-| UnsafeResourceGet.java:41:20:41:22 | url | semmle.label | url |
-| UnsafeResourceGet.java:111:24:111:58 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:115:68:115:78 | requestPath | semmle.label | requestPath |
-| UnsafeResourceGet.java:143:23:143:56 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:148:13:148:46 | getResource(...) : URL | semmle.label | getResource(...) : URL |
-| UnsafeResourceGet.java:148:36:148:45 | requestUrl : String | semmle.label | requestUrl : String |
-| UnsafeResourceGet.java:150:20:150:22 | url | semmle.label | url |
-| UnsafeResourceGet.java:181:24:181:58 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:189:68:189:78 | requestPath | semmle.label | requestPath |
-| UnsafeResourceGet.java:219:23:219:56 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:224:13:224:63 | getResource(...) : URL | semmle.label | getResource(...) : URL |
-| UnsafeResourceGet.java:224:53:224:62 | requestUrl : String | semmle.label | requestUrl : String |
-| UnsafeResourceGet.java:226:20:226:22 | url | semmle.label | url |
-| UnsafeResourceGet.java:237:24:237:58 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:241:18:241:44 | getResource(...) : Resource | semmle.label | getResource(...) : Resource |
-| UnsafeResourceGet.java:241:33:241:43 | requestPath : String | semmle.label | requestPath : String |
-| UnsafeResourceGet.java:245:21:245:22 | rs : Resource | semmle.label | rs : Resource |
-| UnsafeResourceGet.java:245:21:245:32 | getPath(...) | semmle.label | getPath(...) |
-| UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | semmle.label | returnURL |
-| UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | semmle.label | returnURL |
-| UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeServletRequestDispatch.java:76:53:76:56 | path | semmle.label | path |
-| UnsafeUrlForward.java:13:27:13:36 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:14:27:14:29 | url | semmle.label | url |
-| UnsafeUrlForward.java:18:27:18:36 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:20:28:20:30 | url | semmle.label | url |
-| UnsafeUrlForward.java:25:21:25:30 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:26:23:26:25 | url | semmle.label | url |
-| UnsafeUrlForward.java:30:27:30:36 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:31:48:31:63 | ... + ... | semmle.label | ... + ... |
-| UnsafeUrlForward.java:31:61:31:63 | url | semmle.label | url |
-| UnsafeUrlForward.java:36:19:36:28 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:38:33:38:35 | url | semmle.label | url |
-| UnsafeUrlForward.java:47:19:47:28 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:49:33:49:62 | ... + ... | semmle.label | ... + ... |
-| UnsafeUrlForward.java:58:19:58:28 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:60:33:60:62 | ... + ... | semmle.label | ... + ... |
-subpaths
-#select
-| UnsafeLoadSpringResource.java:35:31:35:33 | clr | UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | UnsafeLoadSpringResource.java:35:31:35:33 | clr | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:27:32:27:77 | fileName | user-provided value |
-| UnsafeLoadSpringResource.java:76:38:76:45 | fileName | UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | UnsafeLoadSpringResource.java:76:38:76:45 | fileName | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:68:32:68:77 | fileName | user-provided value |
-| UnsafeLoadSpringResource.java:116:51:116:58 | fileName | UnsafeLoadSpringResource.java:108:32:108:77 | fileName : String | UnsafeLoadSpringResource.java:116:51:116:58 | fileName | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:108:32:108:77 | fileName | user-provided value |
-| UnsafeRequestPath.java:23:33:23:36 | path | UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | UnsafeRequestPath.java:23:33:23:36 | path | Potentially untrusted URL forward due to $@. | UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) | user-provided value |
-| UnsafeResourceGet2.java:19:93:19:99 | loadUrl | UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:19:93:19:99 | loadUrl | Potentially untrusted URL forward due to $@. | UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) | user-provided value |
-| UnsafeResourceGet2.java:37:20:37:22 | url | UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:37:20:37:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) | user-provided value |
-| UnsafeResourceGet.java:41:20:41:22 | url | UnsafeResourceGet.java:32:23:32:56 | getParameter(...) : String | UnsafeResourceGet.java:41:20:41:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:32:23:32:56 | getParameter(...) | user-provided value |
-| UnsafeResourceGet.java:115:68:115:78 | requestPath | UnsafeResourceGet.java:111:24:111:58 | getParameter(...) : String | UnsafeResourceGet.java:115:68:115:78 | requestPath | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:111:24:111:58 | getParameter(...) | user-provided value |
-| UnsafeResourceGet.java:150:20:150:22 | url | UnsafeResourceGet.java:143:23:143:56 | getParameter(...) : String | UnsafeResourceGet.java:150:20:150:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:143:23:143:56 | getParameter(...) | user-provided value |
-| UnsafeResourceGet.java:189:68:189:78 | requestPath | UnsafeResourceGet.java:181:24:181:58 | getParameter(...) : String | UnsafeResourceGet.java:189:68:189:78 | requestPath | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:181:24:181:58 | getParameter(...) | user-provided value |
-| UnsafeResourceGet.java:226:20:226:22 | url | UnsafeResourceGet.java:219:23:219:56 | getParameter(...) : String | UnsafeResourceGet.java:226:20:226:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:219:23:219:56 | getParameter(...) | user-provided value |
-| UnsafeResourceGet.java:245:21:245:32 | getPath(...) | UnsafeResourceGet.java:237:24:237:58 | getParameter(...) : String | UnsafeResourceGet.java:245:21:245:32 | getPath(...) | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:237:24:237:58 | getParameter(...) | user-provided value |
-| UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) | user-provided value |
-| UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) | user-provided value |
-| UnsafeServletRequestDispatch.java:76:53:76:56 | path | UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | UnsafeServletRequestDispatch.java:76:53:76:56 | path | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) | user-provided value |
-| UnsafeUrlForward.java:14:27:14:29 | url | UnsafeUrlForward.java:13:27:13:36 | url : String | UnsafeUrlForward.java:14:27:14:29 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:13:27:13:36 | url | user-provided value |
-| UnsafeUrlForward.java:20:28:20:30 | url | UnsafeUrlForward.java:18:27:18:36 | url : String | UnsafeUrlForward.java:20:28:20:30 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:18:27:18:36 | url | user-provided value |
-| UnsafeUrlForward.java:26:23:26:25 | url | UnsafeUrlForward.java:25:21:25:30 | url : String | UnsafeUrlForward.java:26:23:26:25 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:25:21:25:30 | url | user-provided value |
-| UnsafeUrlForward.java:31:48:31:63 | ... + ... | UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:48:31:63 | ... + ... | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:30:27:30:36 | url | user-provided value |
-| UnsafeUrlForward.java:31:61:31:63 | url | UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:61:31:63 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:30:27:30:36 | url | user-provided value |
-| UnsafeUrlForward.java:38:33:38:35 | url | UnsafeUrlForward.java:36:19:36:28 | url : String | UnsafeUrlForward.java:38:33:38:35 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:36:19:36:28 | url | user-provided value |
-| UnsafeUrlForward.java:49:33:49:62 | ... + ... | UnsafeUrlForward.java:47:19:47:28 | url : String | UnsafeUrlForward.java:49:33:49:62 | ... + ... | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:47:19:47:28 | url | user-provided value |
-| UnsafeUrlForward.java:60:33:60:62 | ... + ... | UnsafeUrlForward.java:58:19:58:28 | url : String | UnsafeUrlForward.java:60:33:60:62 | ... + ... | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:58:19:58:28 | url | user-provided value |
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java
index 4018ed289481..0a00637cd44c 100644
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java
@@ -11,31 +11,31 @@ public class UnsafeUrlForward {
@GetMapping("/bad1")
public ModelAndView bad1(String url) {
- return new ModelAndView(url);
+ return new ModelAndView(url); // $ hasUnsafeUrlForward
}
@GetMapping("/bad2")
public ModelAndView bad2(String url) {
ModelAndView modelAndView = new ModelAndView();
- modelAndView.setViewName(url);
+ modelAndView.setViewName(url); // $ hasUnsafeUrlForward
return modelAndView;
}
@GetMapping("/bad3")
public String bad3(String url) {
- return "forward:" + url + "/swagger-ui/index.html";
+ return "forward:" + url + "/swagger-ui/index.html"; // $ hasUnsafeUrlForward
}
@GetMapping("/bad4")
public ModelAndView bad4(String url) {
- ModelAndView modelAndView = new ModelAndView("forward:" + url);
+ ModelAndView modelAndView = new ModelAndView("forward:" + url); // $ hasUnsafeUrlForward
return modelAndView;
}
@GetMapping("/bad5")
public void bad5(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher(url).include(request, response);
+ request.getRequestDispatcher(url).include(request, response); // $ hasUnsafeUrlForward
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
@@ -46,7 +46,7 @@ public void bad5(String url, HttpServletRequest request, HttpServletResponse res
@GetMapping("/bad6")
public void bad6(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").include(request, response);
+ request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").include(request, response); // $ hasUnsafeUrlForward
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
@@ -57,7 +57,7 @@ public void bad6(String url, HttpServletRequest request, HttpServletResponse res
@GetMapping("/bad7")
public void bad7(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").forward(request, response);
+ request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").forward(request, response); // $ hasUnsafeUrlForward
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.qlref b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.qlref
deleted file mode 100644
index 934a18cc6c78..000000000000
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.qlref
+++ /dev/null
@@ -1 +0,0 @@
-experimental/Security/CWE/CWE-552/UnsafeUrlForward.ql
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.expected b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.expected
new file mode 100644
index 000000000000..8ec8033d086e
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.expected
@@ -0,0 +1,2 @@
+testFailures
+failures
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.ql b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.ql
new file mode 100644
index 000000000000..cf77edcf12a4
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.ql
@@ -0,0 +1,18 @@
+import java
+import TestUtilities.InlineExpectationsTest
+import semmle.code.java.security.UnsafeUrlForwardQuery
+
+module UnsafeUrlForwardTest implements TestSig {
+ string getARelevantTag() { result = "hasUnsafeUrlForward" }
+
+ predicate hasActualResult(Location location, string element, string tag, string value) {
+ tag = "hasUnsafeUrlForward" and
+ exists(UnsafeUrlForwardFlow::PathNode sink | UnsafeUrlForwardFlow::flowPath(_, sink) |
+ location = sink.getNode().getLocation() and
+ element = sink.getNode().toString() and
+ value = ""
+ )
+ }
+}
+
+import MakeTest
From 915e106ab382aaa060e11b02e34d61c7434452c6 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 20 Nov 2023 18:10:07 -0500
Subject: [PATCH 04/40] Java: remove path-injection related models and tests
for now
---
...unsafeUrlForwardExperimentalMove.model.yml | 40 +--
.../code/java/security/UnsafeUrlForward.qll | 91 +-----
.../java/security/UnsafeUrlForwardQuery.qll | 24 +-
.../CWE-552/UnsafeLoadSpringResource.java | 155 ----------
.../security/CWE-552/UnsafeResourceGet.java | 270 ------------------
.../security/CWE-552/UnsafeResourceGet2.java | 58 ----
6 files changed, 12 insertions(+), 626 deletions(-)
delete mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
delete mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java
delete mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java
diff --git a/java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml b/java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml
index b48d891e692d..27c1094d765b 100644
--- a/java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml
+++ b/java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml
@@ -6,56 +6,18 @@ extensions:
- ["jakarta.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual"]
- ["javax.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual"]
- # # ! below added by me when debugging CVEs:
- # - ["org.springframework.cloud.config.server.resource", "ResourceController", True, "retrieve", "(String,String,String,ServletWebRequest,boolean)", "", "Parameter[3]", "remote", "manual"]
- # - ["org.springframework.web.context.request", "ServletWebRequest", True, "getContextPath", "()", "", "ReturnValue", "remote", "manual"]
-
- addsTo:
pack: codeql/java-all
extensible: sinkModel
data:
- ["java.util.concurrent", "TimeUnit", True, "sleep", "", "", "Argument[0]", "thread-pause", "manual"] # ! this seems like a typo; doesn't look like it's used in the query at all
- - ["org.springframework.core.io", "ClassPathResource", True, "getFilename", "", "", "Argument[this]", "get-resource", "manual"] # ! Note: `ClassPathResource` implements `Resource`, so it might make more sense to model some of these as `Resource` with subtype True.
- - ["org.springframework.core.io", "ClassPathResource", True, "getPath", "", "", "Argument[this]", "get-resource", "manual"]
- - ["org.springframework.core.io", "ClassPathResource", True, "getURL", "", "", "Argument[this]", "get-resource", "manual"]
- - ["org.springframework.core.io", "ClassPathResource", True, "resolveURL", "", "", "Argument[this]", "get-resource", "manual"]
- # # ! below added by me when debugging CVEs:
- # - ["org.springframework.cloud.config.server.resource", "ResourceController", True, "retrieve", "", "", "Argument[0]", "get-resource", "manual"] # don't need
- # # - ["org.springframework.cloud.config.server.resource", "ResourceController", True, "getFilePath", "", "", "Argument[0..3]", "get-resource", "manual"] # don't need
- # # - ["org.springframework.cloud.config.server.resource", "ResourceRepository", True, "findOne", "", "", "Argument[0..3]", "get-resource", "manual"] # convert to summary
- # # - ["org.springframework.core.io", "InputStreamSource", True, "getInputStream", "", "", "Argument[this]", "get-resource", "manual"] # convert to summary
- # - ["org.springframework.util", "StreamUtils", True, "copyToString", "", "", "Argument[0]", "get-resource", "manual"] # * public class with good docs
- # # - ["org.springframework.cloud.config.server.environment", "SearchPathLocator", True, "getLocations", "(String,String,String)", "", "Argument[0..2]", "get-resource", "manual"] # convert to summary
- # # - ["org.springframework.cloud.config.server.environment", "SearchPathLocator$Locations", True, "getLocations", "()", "", "Argument[this]", "get-resource", "manual"] # convert to summary
- # - ["org.springframework.core.io", "ResourceLoader", True, "getResource", "", "", "Argument[0]", "get-resource", "manual"] # * public interface with good docs, might be problematic for FPs based on fact that the ext contributor changed this to a taint step to avoid "exists" FPS (maybe there's another way to exclude those FPs though).
- # - ["javax.servlet", "ServletContext", True, "getResource", "", "", "Argument[0]", "get-resource", "manual"]
- # - ["javax.servlet", "ServletContext", True, "getResourceAsStream", "", "", "Argument[0]", "get-resource", "manual"]
- # - ["javax.servlet", "ServletContext", True, "getResourcePaths", "", "", "Argument[0]", "get-resource", "manual"]
- # - ["javax.servlet", "ServletContext", True, "getResource", "", "", "Argument[this]", "get-resource", "manual"]
- # - ["javax.servlet", "ServletContext", True, "getResourceAsStream", "", "", "Argument[this]", "get-resource", "manual"]
- # - ["javax.servlet", "ServletContext", True, "getResourcePaths", "", "", "Argument[this]", "get-resource", "manual"]
- # # - ["org.apache.tomcat.util.http", "RequestUtil", True, "normalize", "", "", "Argument[0]", "get-resource", "manual"]
-
- addsTo:
pack: codeql/java-all
extensible: summaryModel
data:
- - ["io.undertow.server.handlers.resource", "Resource", True, "getFile", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
- - ["io.undertow.server.handlers.resource", "Resource", True, "getFilePath", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
- - ["io.undertow.server.handlers.resource", "Resource", True, "getPath", "", "", "Argument[this]", "ReturnValue", "taint", "manual"] # ! this as a taint step seems to contradict the fact that they did `ClassPathResource.getPath` as a sink for Spring...
- - ["java.nio.file", "Path", True, "normalize", "", "", "Argument[this]", "ReturnValue", "taint", "manual"] # ! shouldn't this be a sanitizer instead??? Or no because WEB-INF ones don't care about normalization?
+ - ["java.nio.file", "Path", True, "normalize", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
- ["java.nio.file", "Path", True, "resolve", "", "", "Argument[this]", "ReturnValue", "taint", "manual"] # ! check if this and the below are already in the default models
- ["java.nio.file", "Path", True, "resolve", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
- ["java.nio.file", "Path", True, "toString", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
- ["java.nio.file", "Paths", True, "get", "", "", "Argument[0..1]", "ReturnValue", "taint", "manual"]
-
- - ["org.springframework.core.io", "ClassPathResource", False, "ClassPathResource", "", "", "Argument[0]", "Argument[this]", "taint", "manual"]
- - ["org.springframework.core.io", "Resource", True, "createRelative", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
- # # ! below added/modified by me when debugging CVEs:
- - ["org.springframework.core.io", "ResourceLoader", True, "getResource", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
- # - ["org.springframework.cloud.config.server.resource", "ResourceRepository", True, "findOne", "", "", "Argument[0..3]", "ReturnValue", "taint", "manual"] # * public interface, but might be too specific, no easily findable docs...
- # - ["org.springframework.core.io", "InputStreamSource", True, "getInputStream", "", "", "Argument[this]", "ReturnValue", "taint", "manual"] # * public interface with good docs, Note: other `getInputStream`s are remote source and/or taint step, so this as taint step versus sink probably is more consistent
- # - ["org.springframework.cloud.config.server.environment", "SearchPathLocator", True, "getLocations", "(String,String,String)", "", "Argument[0..2]", "ReturnValue", "taint", "manual"] # * public interface with docs: https://www.javadoc.io/static/org.springframework.cloud/spring-cloud-config-server/2.1.0.RELEASE/org/springframework/cloud/config/server/environment/SearchPathLocator.html
- # - ["org.springframework.cloud.config.server.environment", "SearchPathLocator$Locations", True, "getLocations", "()", "", "Argument[this]", "ReturnValue", "taint", "manual"] # ! is the `Locations` class package-private? Or does it inherit public from it's enclosing interface?
- # - ["org.springframework.cloud.config.server.resource", "ResourceController", True, "getFilePath", "", "", "Argument[0..3]", "ReturnValue", "taint", "manual"] # don't need
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
index 48b4431015e4..dd3e17aa8321 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
@@ -22,96 +22,7 @@ private class RequestDispatcherSink extends UnsafeUrlForwardSink {
}
}
-/** 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()
- )
- }
-}
-
+// TODO: look into `StaplerResponse.forward`, etc., and think about re-adding the MaD "request-forgery" sinks as a result
/** An argument to `new ModelAndView` or `ModelAndView.setViewName`. */
private class SpringModelAndViewSink extends UnsafeUrlForwardSink {
SpringModelAndViewSink() {
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
index 9ee3f2ab4170..6cd419a5e131 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
@@ -1,3 +1,5 @@
+/** Provides configurations to be used in queries related to unsafe URL forwarding. */
+
import java
import semmle.code.java.security.UnsafeUrlForward
import semmle.code.java.dataflow.FlowSources
@@ -5,9 +7,13 @@ import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.Jsf
import semmle.code.java.security.PathSanitizer
+/**
+ * A taint-tracking configuration for untrusted user input in a URL forward or include.
+ */
module UnsafeUrlForwardFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof ThreatModelFlowSource and
+ // TODO: move below logic to class in UnsafeUrlForward.qll?
not exists(MethodCall ma, Method m | ma.getMethod() = m |
(
m instanceof HttpServletRequestGetRequestUriMethod or
@@ -25,21 +31,11 @@ module UnsafeUrlForwardFlowConfig implements DataFlow::ConfigSig {
node instanceof PathInjectionSanitizer
}
+ // TODO: check if the below is still needed after removing path-injection related sinks.
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()
- )
- }
}
+/**
+ * Taint-tracking flow for untrusted user input in a URL forward or include.
+ */
module UnsafeUrlForwardFlow = TaintTracking::Global;
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java b/java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
deleted file mode 100644
index 363d84cabe90..000000000000
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package com.example;
-
-import java.io.File;
-import java.io.FileReader;
-import java.io.InputStreamReader;
-import java.io.IOException;
-import java.io.Reader;
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.nio.file.Files;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.core.io.ClassPathResource;
-import org.springframework.core.io.Resource;
-import org.springframework.core.io.ResourceLoader;
-import org.springframework.util.ResourceUtils;
-import org.springframework.util.StringUtils;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-/** Sample class of Spring RestController */
-@RestController
-public class UnsafeLoadSpringResource {
- @GetMapping("/file1")
- //BAD: Get resource from ClassPathResource without input validation
- public String getFileContent1(@RequestParam(name="fileName") String fileName) {
- // A request such as the following can disclose source code and application configuration
- // fileName=/../../WEB-INF/views/page.jsp
- // fileName=/com/example/package/SampleController.class
- ClassPathResource clr = new ClassPathResource(fileName);
- char[] buffer = new char[4096];
- StringBuilder out = new StringBuilder();
- try {
- Reader in = new FileReader(clr.getFilename()); // $ hasUnsafeUrlForward (path-inj?)
- for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
- out.append(buffer, 0, numRead);
- }
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- return out.toString();
- }
-
- @GetMapping("/file1a")
- //GOOD: Get resource from ClassPathResource with input path validation
- public String getFileContent1a(@RequestParam(name="fileName") String fileName) {
- String result = null;
- if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
- ClassPathResource clr = new ClassPathResource(fileName);
- char[] buffer = new char[4096];
- StringBuilder out = new StringBuilder();
- try {
- Reader in = new InputStreamReader(clr.getInputStream(), "UTF-8");
- for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
- out.append(buffer, 0, numRead);
- }
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- result = out.toString();
- }
- return result;
- }
-
- @GetMapping("/file2")
- //BAD: Get resource from ResourceUtils without input validation
- public String getFileContent2(@RequestParam(name="fileName") String fileName) {
- String content = null;
-
- try {
- // A request such as the following can disclose source code and system configuration
- // fileName=/etc/hosts
- // fileName=file:/etc/hosts
- // fileName=/opt/appdir/WEB-INF/views/page.jsp
- File file = ResourceUtils.getFile(fileName); // $ hasUnsafeUrlForward (path-inj?)
- //Read File Content
- content = new String(Files.readAllBytes(file.toPath()));
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- return content;
- }
-
- @GetMapping("/file2a")
- //GOOD: Get resource from ResourceUtils with input path validation
- public String getFileContent2a(@RequestParam(name="fileName") String fileName) {
- String content = null;
-
- if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
- try {
- File file = ResourceUtils.getFile(fileName);
- //Read File Content
- content = new String(Files.readAllBytes(file.toPath()));
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- }
- return content;
- }
-
- @Autowired
- ResourceLoader resourceLoader;
-
- @GetMapping("/file3")
- //BAD: Get resource from ResourceLoader (same as application context) without input validation
- // Note it is not detected without the generic `resource.getInputStream()` check
- public String getFileContent3(@RequestParam(name="fileName") String fileName) {
- String content = null;
-
- try {
- // A request such as the following can disclose source code and system configuration
- // fileName=/WEB-INF/views/page.jsp
- // fileName=/WEB-INF/classes/com/example/package/SampleController.class
- // fileName=file:/etc/hosts
- Resource resource = resourceLoader.getResource(fileName); // $ hasUnsafeUrlForward (path-inj?)
-
- char[] buffer = new char[4096];
- StringBuilder out = new StringBuilder();
-
- Reader in = new InputStreamReader(resource.getInputStream(), "UTF-8");
- for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
- out.append(buffer, 0, numRead);
- }
- content = out.toString();
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- return content;
- }
-
- @GetMapping("/file3a")
- //GOOD: Get resource from ResourceLoader (same as application context) with input path validation
- public String getFileContent3a(@RequestParam(name="fileName") String fileName) {
- String content = null;
-
- if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
- try {
- Resource resource = resourceLoader.getResource(fileName);
-
- char[] buffer = new char[4096];
- StringBuilder out = new StringBuilder();
-
- Reader in = new InputStreamReader(resource.getInputStream(), "UTF-8");
- for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
- out.append(buffer, 0, numRead);
- }
- content = out.toString();
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- }
- return content;
- }
-}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java b/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java
deleted file mode 100644
index 053887984c61..000000000000
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet.java
+++ /dev/null
@@ -1,270 +0,0 @@
-package com.example;
-
-import java.io.InputStream;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.net.URI;
-import java.net.URL;
-import java.net.URISyntaxException;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.ServletOutputStream;
-import javax.servlet.ServletException;
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletContext;
-
-import io.undertow.server.handlers.resource.FileResourceManager;
-import io.undertow.server.handlers.resource.Resource;
-import org.jboss.vfs.VFS;
-import org.jboss.vfs.VirtualFile;
-
-public class UnsafeResourceGet extends HttpServlet {
- private static final String BASE_PATH = "/pages";
-
- @Override
- // BAD: getResource constructed from `ServletContext` without input validation
- protected void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- ServletConfig cfg = getServletConfig();
- ServletContext sc = cfg.getServletContext();
-
- // A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
- URL url = sc.getResource(requestUrl);
-
- InputStream in = url.openStream(); // $ hasUnsafeUrlForward (SSRF)
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- // GOOD: getResource constructed from `ServletContext` with input validation
- protected void doGetGood(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- ServletConfig cfg = getServletConfig();
- ServletContext sc = cfg.getServletContext();
-
- Path path = Paths.get(requestUrl).normalize().toRealPath();
- if (path.startsWith(BASE_PATH)) {
- URL url = sc.getResource(path.toString());
-
- InputStream in = url.openStream();
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
- }
-
- // GOOD: getResource constructed from `ServletContext` with null check only
- protected void doGetGood2(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- PrintWriter writer = response.getWriter();
-
- ServletConfig cfg = getServletConfig();
- ServletContext sc = cfg.getServletContext();
-
- // A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
- URL url = sc.getResource(requestUrl);
- if (url == null) {
- writer.println("Requested source not found");
- }
- }
-
- // GOOD: getResource constructed from `ServletContext` with `equals` check
- protected void doGetGood3(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- ServletContext sc = request.getServletContext();
-
- if (requestUrl.equals("/public/crossdomain.xml")) {
- URL url = sc.getResource(requestUrl);
-
- InputStream in = url.openStream();
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
- }
-
- @Override
- // BAD: getResourceAsStream constructed from `ServletContext` without input validation
- protected void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
- ServletOutputStream out = response.getOutputStream();
-
- // A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
- InputStream in = request.getServletContext().getResourceAsStream(requestPath); // $ hasUnsafeUrlForward (path-inj?)
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- // GOOD: getResourceAsStream constructed from `ServletContext` with input validation
- protected void doPostGood(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
- ServletOutputStream out = response.getOutputStream();
-
- if (!requestPath.contains("..") && requestPath.startsWith("/trusted")) {
- InputStream in = request.getServletContext().getResourceAsStream(requestPath);
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
- }
-
- @Override
- // BAD: getResource constructed from `Class` without input validation
- protected void doHead(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- // A sample request /fake.jsp/../../../WEB-INF/web.xml can load the web.xml file
- // Note the class is in two levels of subpackages and `Class.getResource` starts from its own directory
- URL url = getClass().getResource(requestUrl);
-
- InputStream in = url.openStream(); // $ hasUnsafeUrlForward (SSRF)
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- // GOOD: getResource constructed from `Class` with input validation
- protected void doHeadGood(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- Path path = Paths.get(requestUrl).normalize().toRealPath();
- if (path.startsWith(BASE_PATH)) {
- URL url = getClass().getResource(path.toString());
-
- InputStream in = url.openStream();
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
- }
-
- @Override
- // BAD: getResourceAsStream constructed from `ClassLoader` without input validation
- protected void doPut(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
- ServletOutputStream out = response.getOutputStream();
-
- ServletConfig cfg = getServletConfig();
- ServletContext sc = cfg.getServletContext();
-
- // A sample request /fake.jsp/../../../WEB-INF/web.xml can load the web.xml file
- // Note the class is in two levels of subpackages and `ClassLoader.getResourceAsStream` starts from its own directory
- InputStream in = getClass().getClassLoader().getResourceAsStream(requestPath); // $ hasUnsafeUrlForward (path-inj?)
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- // GOOD: getResourceAsStream constructed from `ClassLoader` with input validation
- protected void doPutGood(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
- ServletOutputStream out = response.getOutputStream();
-
- ServletConfig cfg = getServletConfig();
- ServletContext sc = cfg.getServletContext();
-
- if (!requestPath.contains("..") && requestPath.startsWith("/trusted")) {
- InputStream in = getClass().getClassLoader().getResourceAsStream(requestPath);
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
- }
-
- // BAD: getResource constructed from `ClassLoader` without input validation
- protected void doPutBad(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- // A sample request /fake.jsp/../../../WEB-INF/web.xml can load the web.xml file
- // Note the class is in two levels of subpackages and `ClassLoader.getResource` starts from its own directory
- URL url = getClass().getClassLoader().getResource(requestUrl);
-
- InputStream in = url.openStream(); // $ hasUnsafeUrlForward (SSRF)
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- // BAD: getResource constructed using Undertow IO without input validation
- protected void doPutBad2(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
-
- try {
- FileResourceManager rm = new FileResourceManager(VFS.getChild(new URI("/usr/share")).getPhysicalFile());
- Resource rs = rm.getResource(requestPath);
-
- VirtualFile overlay = VFS.getChild(new URI("EAP_HOME/modules/"));
- // Do file operations
- overlay.getChild(rs.getPath()); // $ hasUnsafeUrlForward (path-inj?)
- } catch (URISyntaxException ue) {
- throw new IOException("Cannot parse the URI");
- }
- }
-
- // GOOD: getResource constructed using Undertow IO with input validation
- protected void doPutGood2(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
-
- try {
- FileResourceManager rm = new FileResourceManager(VFS.getChild(new URI("/usr/share")).getPhysicalFile());
- Resource rs = rm.getResource(requestPath);
-
- VirtualFile overlay = VFS.getChild(new URI("EAP_HOME/modules/"));
- String path = rs.getPath();
- if (path.startsWith("/trusted_path") && !path.contains("..")) {
- // Do file operations
- overlay.getChild(path);
- }
- } catch (URISyntaxException ue) {
- throw new IOException("Cannot parse the URI");
- }
- }
-}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java b/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java
deleted file mode 100644
index 0043bb06c67a..000000000000
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeResourceGet2.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.example;
-
-import javax.faces.context.FacesContext;
-import java.io.BufferedReader;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.IOException;
-import java.net.URL;
-import java.util.Map;
-
-/** Sample class of JSF managed bean */
-public class UnsafeResourceGet2 {
- // BAD: getResourceAsStream constructed from `ExternalContext` without input validation
- public String parameterActionBad1() throws IOException {
- FacesContext fc = FacesContext.getCurrentInstance();
- Map params = fc.getExternalContext().getRequestParameterMap();
- String loadUrl = params.get("loadUrl");
-
- InputStreamReader isr = new InputStreamReader(fc.getExternalContext().getResourceAsStream(loadUrl)); // $ hasUnsafeUrlForward (path-inj?)
- BufferedReader br = new BufferedReader(isr);
- if(br.ready()) {
- //Do Stuff
- return "result";
- }
-
- return "home";
- }
-
- // BAD: getResource constructed from `ExternalContext` without input validation
- public String parameterActionBad2() throws IOException {
- FacesContext fc = FacesContext.getCurrentInstance();
- Map params = fc.getExternalContext().getRequestParameterMap();
- String loadUrl = params.get("loadUrl");
-
- URL url = fc.getExternalContext().getResource(loadUrl);
-
- InputStream in = url.openStream(); // $ hasUnsafeUrlForward (SSRF)
- //Do Stuff
- return "result";
- }
-
- // GOOD: getResource constructed from `ExternalContext` with input validation
- public String parameterActionGood1() throws IOException {
- FacesContext fc = FacesContext.getCurrentInstance();
- Map params = fc.getExternalContext().getRequestParameterMap();
- String loadUrl = params.get("loadUrl");
-
- if (loadUrl.equals("/public/crossdomain.xml")) {
- URL url = fc.getExternalContext().getResource(loadUrl);
-
- InputStream in = url.openStream();
- //Do Stuff
- return "result";
- }
-
- return "home";
- }
-}
From 2a682995aeadbb919d4c400b916c7737fae0b150 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 30 Nov 2023 11:16:39 -0500
Subject: [PATCH 05/40] Java: move MaD models to correct files, delete ones
that already exist
---
.../ql/lib/ext/jakarta.servlet.http.model.yml | 6 +++++
java/ql/lib/ext/javax.servlet.http.model.yml | 2 ++
...unsafeUrlForwardExperimentalMove.model.yml | 23 -------------------
3 files changed, 8 insertions(+), 23 deletions(-)
create mode 100644 java/ql/lib/ext/jakarta.servlet.http.model.yml
delete mode 100644 java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml
diff --git a/java/ql/lib/ext/jakarta.servlet.http.model.yml b/java/ql/lib/ext/jakarta.servlet.http.model.yml
new file mode 100644
index 000000000000..5a83b1ac08d8
--- /dev/null
+++ b/java/ql/lib/ext/jakarta.servlet.http.model.yml
@@ -0,0 +1,6 @@
+extensions:
+ - addsTo:
+ pack: codeql/java-all
+ extensible: sourceModel
+ data:
+ - ["jakarta.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual"]
diff --git a/java/ql/lib/ext/javax.servlet.http.model.yml b/java/ql/lib/ext/javax.servlet.http.model.yml
index afac25e4bc85..ec35445d199a 100644
--- a/java/ql/lib/ext/javax.servlet.http.model.yml
+++ b/java/ql/lib/ext/javax.servlet.http.model.yml
@@ -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
diff --git a/java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml b/java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml
deleted file mode 100644
index 27c1094d765b..000000000000
--- a/java/ql/lib/ext/unsafeUrlForwardExperimentalMove.model.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-extensions:
- - addsTo:
- pack: codeql/java-all
- extensible: sourceModel
- data:
- - ["jakarta.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual"]
- - ["javax.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual"]
-
- - addsTo:
- pack: codeql/java-all
- extensible: sinkModel
- data:
- - ["java.util.concurrent", "TimeUnit", True, "sleep", "", "", "Argument[0]", "thread-pause", "manual"] # ! this seems like a typo; doesn't look like it's used in the query at all
-
- - addsTo:
- pack: codeql/java-all
- extensible: summaryModel
- data:
- - ["java.nio.file", "Path", True, "normalize", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
- - ["java.nio.file", "Path", True, "resolve", "", "", "Argument[this]", "ReturnValue", "taint", "manual"] # ! check if this and the below are already in the default models
- - ["java.nio.file", "Path", True, "resolve", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
- - ["java.nio.file", "Path", True, "toString", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
- - ["java.nio.file", "Paths", True, "get", "", "", "Argument[0..1]", "ReturnValue", "taint", "manual"]
From 4ff884e26cad54486745861fe1145c6539418f8c Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 30 Nov 2023 11:31:39 -0500
Subject: [PATCH 06/40] Java: remove more path-injection related classes (will
maybe add some of these back in a separate PR)
---
java/ql/lib/semmle/code/java/Jsf.qll | 35 -------------------
.../lib/semmle/code/java/SpringResource.qll | 22 ------------
.../code/java/security/UnsafeUrlForward.qll | 2 --
.../java/security/UnsafeUrlForwardQuery.qll | 1 -
4 files changed, 60 deletions(-)
delete mode 100644 java/ql/lib/semmle/code/java/Jsf.qll
delete mode 100644 java/ql/lib/semmle/code/java/SpringResource.qll
diff --git a/java/ql/lib/semmle/code/java/Jsf.qll b/java/ql/lib/semmle/code/java/Jsf.qll
deleted file mode 100644
index 9023953add4b..000000000000
--- a/java/ql/lib/semmle/code/java/Jsf.qll
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Provides classes and predicates for working with the Java Server Faces (JSF).
- */
-
-// TODO: COMBINE WITH EXISTING JSF-RELATED QLL FILES!
-import java
-
-/**
- * The JSF class `ExternalContext` for processing HTTP requests.
- */
-class ExternalContext extends RefType {
- ExternalContext() {
- this.hasQualifiedName(["javax.faces.context", "jakarta.faces.context"], "ExternalContext")
- }
-}
-
-/**
- * The method `getResource()` declared in JSF `ExternalContext`.
- */
-class GetFacesResourceMethod extends Method {
- GetFacesResourceMethod() {
- this.getDeclaringType().getASupertype*() instanceof ExternalContext and
- this.hasName("getResource")
- }
-}
-
-/**
- * The method `getResourceAsStream()` declared in JSF `ExternalContext`.
- */
-class GetFacesResourceAsStreamMethod extends Method {
- GetFacesResourceAsStreamMethod() {
- this.getDeclaringType().getASupertype*() instanceof ExternalContext and
- this.hasName("getResourceAsStream")
- }
-}
diff --git a/java/ql/lib/semmle/code/java/SpringResource.qll b/java/ql/lib/semmle/code/java/SpringResource.qll
deleted file mode 100644
index a7d3a3793b6d..000000000000
--- a/java/ql/lib/semmle/code/java/SpringResource.qll
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Provides classes for working with resource loading in Spring.
- */
-
-// TODO: COMBINE WITH EXISTING SPRING-RELATED QLL FILES!
-import java
-private import semmle.code.java.dataflow.FlowSources
-
-/** A utility class for resolving resource locations to files in the file system in the Spring framework. */
-class ResourceUtils extends Class {
- ResourceUtils() { this.hasQualifiedName("org.springframework.util", "ResourceUtils") }
-}
-
-/**
- * A method declared in `org.springframework.util.ResourceUtils` that loads Spring resources.
- */
-class GetResourceUtilsMethod extends Method {
- GetResourceUtilsMethod() {
- this.getDeclaringType().getASupertype*() instanceof ResourceUtils and
- this.hasName(["extractArchiveURL", "extractJarFileURL", "getFile", "getURL"])
- }
-}
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
index dd3e17aa8321..628397d07ef9 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
@@ -1,10 +1,8 @@
import java
-private import semmle.code.java.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 semmle.code.java.SpringResource
/** A sink for unsafe URL forward vulnerabilities. */
abstract class UnsafeUrlForwardSink extends DataFlow::Node { }
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
index 6cd419a5e131..4231af1a90a2 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
@@ -4,7 +4,6 @@ import java
import semmle.code.java.security.UnsafeUrlForward
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
-import semmle.code.java.Jsf
import semmle.code.java.security.PathSanitizer
/**
From 42e3825ea3b50ee362ae723c657744de85860e5e Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 5 Mar 2024 11:55:54 -0500
Subject: [PATCH 07/40] Java: convert RequestDispatcherSink to MaD
---
java/ql/lib/ext/jakarta.servlet.model.yml | 8 ++++++++
java/ql/lib/ext/javax.portlet.model.yml | 7 +++++++
java/ql/lib/ext/javax.servlet.model.yml | 3 +++
.../semmle/code/java/security/UnsafeUrlForward.qll | 11 +++--------
4 files changed, 21 insertions(+), 8 deletions(-)
create mode 100644 java/ql/lib/ext/jakarta.servlet.model.yml
create mode 100644 java/ql/lib/ext/javax.portlet.model.yml
diff --git a/java/ql/lib/ext/jakarta.servlet.model.yml b/java/ql/lib/ext/jakarta.servlet.model.yml
new file mode 100644
index 000000000000..fc1274cadaf6
--- /dev/null
+++ b/java/ql/lib/ext/jakarta.servlet.model.yml
@@ -0,0 +1,8 @@
+extensions:
+ - addsTo:
+ pack: codeql/java-all
+ extensible: sinkModel
+ data:
+ # TODO: potentially switch to using Argument[this] of `RequestDispatcher.forward|include` as sink instead of the below.
+ - ["jakarta.servlet", "ServletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
+ - ["jakarta.servlet", "ServletRequest", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
diff --git a/java/ql/lib/ext/javax.portlet.model.yml b/java/ql/lib/ext/javax.portlet.model.yml
new file mode 100644
index 000000000000..e39484abcb74
--- /dev/null
+++ b/java/ql/lib/ext/javax.portlet.model.yml
@@ -0,0 +1,7 @@
+extensions:
+ - addsTo:
+ pack: codeql/java-all
+ extensible: sinkModel
+ data:
+ # TODO: potentially switch to using Argument[this] of `PortletRequestDispatcher.forward|include` as sink instead of the below.
+ - ["javax.portlet", "PortletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
diff --git a/java/ql/lib/ext/javax.servlet.model.yml b/java/ql/lib/ext/javax.servlet.model.yml
index 581863c74f7e..7c405ac0de91 100644
--- a/java/ql/lib/ext/javax.servlet.model.yml
+++ b/java/ql/lib/ext/javax.servlet.model.yml
@@ -14,6 +14,9 @@ extensions:
extensible: sinkModel
data:
- ["javax.servlet", "ServletContext", True, "getResourceAsStream", "(String)", "", "Argument[0]", "path-injection", "ai-manual"]
+ # TODO: potentially switch to using Argument[this] of `RequestDispatcher.forward|include` as sink instead of the below.
+ - ["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
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
index 628397d07ef9..e7780ee971b6 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
@@ -10,14 +10,9 @@ 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()
- )
- }
+/** A default sink representing methods susceptible to unsafe URL forwarding. */
+private class DefaultUnsafeUrlForwardSink extends UnsafeUrlForwardSink {
+ DefaultUnsafeUrlForwardSink() { sinkNode(this, "url-forward") }
}
// TODO: look into `StaplerResponse.forward`, etc., and think about re-adding the MaD "request-forgery" sinks as a result
From 8d66097483a8892d4a15c2232e63d883035a45f9 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 30 Nov 2023 13:50:51 -0500
Subject: [PATCH 08/40] Java: switch StaplerResponse.forward from
request-forgery sink to url-forward sink
---
java/ql/lib/ext/org.kohsuke.stapler.model.yml | 2 +-
java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/java/ql/lib/ext/org.kohsuke.stapler.model.yml b/java/ql/lib/ext/org.kohsuke.stapler.model.yml
index 63bbdbfd52a6..ca9f08ba78c9 100644
--- a/java/ql/lib/ext/org.kohsuke.stapler.model.yml
+++ b/java/ql/lib/ext/org.kohsuke.stapler.model.yml
@@ -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"]
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
index e7780ee971b6..0e96066c72e1 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
@@ -15,7 +15,6 @@ private class DefaultUnsafeUrlForwardSink extends UnsafeUrlForwardSink {
DefaultUnsafeUrlForwardSink() { sinkNode(this, "url-forward") }
}
-// TODO: look into `StaplerResponse.forward`, etc., and think about re-adding the MaD "request-forgery" sinks as a result
/** An argument to `new ModelAndView` or `ModelAndView.setViewName`. */
private class SpringModelAndViewSink extends UnsafeUrlForwardSink {
SpringModelAndViewSink() {
From 1da1e896cbc8eb1b918c8c4950010b53146391a6 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 5 Mar 2024 12:25:19 -0500
Subject: [PATCH 09/40] Java: convert SpringModelAndViewSink to MaD
---
.../ext/org.springframework.web.portlet.model.yml | 7 +++++++
.../ext/org.springframework.web.servlet.model.yml | 7 +++++++
.../semmle/code/java/security/UnsafeUrlForward.qll | 12 ------------
shared/mad/codeql/mad/ModelValidation.qll | 4 ++--
4 files changed, 16 insertions(+), 14 deletions(-)
create mode 100644 java/ql/lib/ext/org.springframework.web.portlet.model.yml
create mode 100644 java/ql/lib/ext/org.springframework.web.servlet.model.yml
diff --git a/java/ql/lib/ext/org.springframework.web.portlet.model.yml b/java/ql/lib/ext/org.springframework.web.portlet.model.yml
new file mode 100644
index 000000000000..ba90b531c33e
--- /dev/null
+++ b/java/ql/lib/ext/org.springframework.web.portlet.model.yml
@@ -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"]
diff --git a/java/ql/lib/ext/org.springframework.web.servlet.model.yml b/java/ql/lib/ext/org.springframework.web.servlet.model.yml
new file mode 100644
index 000000000000..acdda3d569f6
--- /dev/null
+++ b/java/ql/lib/ext/org.springframework.web.servlet.model.yml
@@ -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"]
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
index 0e96066c72e1..cd65a6f63451 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
@@ -15,18 +15,6 @@ private class DefaultUnsafeUrlForwardSink extends UnsafeUrlForwardSink {
DefaultUnsafeUrlForwardSink() { sinkNode(this, "url-forward") }
}
-/** 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 {
PrimitiveSanitizer() {
this.getType() instanceof PrimitiveType or
diff --git a/shared/mad/codeql/mad/ModelValidation.qll b/shared/mad/codeql/mad/ModelValidation.qll
index bb3b8c174b97..20bcdd1908cd 100644
--- a/shared/mad/codeql/mad/ModelValidation.qll
+++ b/shared/mad/codeql/mad/ModelValidation.qll
@@ -33,8 +33,8 @@ module KindValidation {
"bean-validation", "fragment-injection", "groovy-injection", "hostname-verification",
"information-leak", "intent-redirection", "jexl-injection", "jndi-injection",
"mvel-injection", "notification", "ognl-injection", "pending-intents",
- "response-splitting", "trust-boundary-violation", "template-injection", "xpath-injection",
- "xslt-injection",
+ "response-splitting", "trust-boundary-violation", "template-injection", "url-forward",
+ "xpath-injection", "xslt-injection",
// JavaScript-only currently, but may be shared in the future
"mongodb.sink", "nosql-injection", "unsafe-deserialization",
// Swift-only currently, but may be shared in the future
From 5a9d7552b3d92aba8cdee9a75ea95c02819bb4cf Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 30 Nov 2023 16:05:59 -0500
Subject: [PATCH 10/40] Java: add some comments and minor code reorg
---
.../code/java/security/UnsafeUrlForward.qll | 54 ++++++++++---------
.../java/security/UnsafeUrlForwardQuery.qll | 2 +-
2 files changed, 30 insertions(+), 26 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
index cd65a6f63451..4a529896f866 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
@@ -1,20 +1,40 @@
+/** Provides classes related to unsafe URL forwarding in Java. */
+
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.frameworks.javaee.ejb.EJBRestrictions
/** 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 { }
-
/** A default sink representing methods susceptible to unsafe URL forwarding. */
private class DefaultUnsafeUrlForwardSink extends UnsafeUrlForwardSink {
DefaultUnsafeUrlForwardSink() { sinkNode(this, "url-forward") }
}
+/**
+ * An expression appended (perhaps indirectly) to `"forward:"`, and which
+ * is reachable from a Spring entry point.
+ */
+private class SpringUrlForwardSink extends UnsafeUrlForwardSink {
+ SpringUrlForwardSink() {
+ // TODO: check if can use MaD "Annotated" for `SpringRequestMappingMethod` or if too complicated for MaD (probably too complicated).
+ any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable()) and
+ this.asExpr() = any(ForwardPrefix fp).getAnAppendedExpression()
+ }
+}
+
+// TODO: should this potentially be "include:" as well? Or does that not work similarly?
+private class ForwardPrefix extends InterestingPrefix {
+ ForwardPrefix() { this.getStringValue() = "forward:" }
+
+ override int getOffset() { result = 0 }
+}
+
+/** A sanitizer for unsafe URL forward vulnerabilities. */
+abstract class UnsafeUrlForwardSanitizer extends DataFlow::Node { }
+
private class PrimitiveSanitizer extends UnsafeUrlForwardSanitizer {
PrimitiveSanitizer() {
this.getType() instanceof PrimitiveType or
@@ -23,6 +43,11 @@ private class PrimitiveSanitizer extends UnsafeUrlForwardSanitizer {
}
}
+// TODO: double-check this sanitizer (and should I switch all "sanitizer" naming to "barrier" instead?)
+private class FollowsSanitizingPrefix extends UnsafeUrlForwardSanitizer {
+ FollowsSanitizingPrefix() { this.asExpr() = any(SanitizingPrefix fp).getAnAppendedExpression() }
+}
+
private class SanitizingPrefix extends InterestingPrefix {
SanitizingPrefix() {
not this.getStringValue().matches("/WEB-INF/%") and
@@ -31,24 +56,3 @@ private class SanitizingPrefix extends InterestingPrefix {
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()
- }
-}
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll b/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
index 4231af1a90a2..496702345820 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
@@ -12,7 +12,7 @@ import semmle.code.java.security.PathSanitizer
module UnsafeUrlForwardFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof ThreatModelFlowSource and
- // TODO: move below logic to class in UnsafeUrlForward.qll?
+ // TODO: move below logic to class in UnsafeUrlForward.qll? And check exactly why these were excluded.
not exists(MethodCall ma, Method m | ma.getMethod() = m |
(
m instanceof HttpServletRequestGetRequestUriMethod or
From 6e7c05467bddf44f0411920392b6886f680c5ecf Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Fri, 1 Dec 2023 08:29:17 -0500
Subject: [PATCH 11/40] Java: update query metadata and alert message
---
.../Security/CWE/CWE-552/UnsafeUrlForward.ql | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
index 4e3326a831ee..06686d5e0bd0 100644
--- a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
+++ b/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
@@ -1,13 +1,16 @@
/**
- * @name Unsafe URL forward or include from a remote source
- * @description URL forward or include based on unvalidated user-input
- * may cause file information disclosure.
+ * @name URL forward from a remote source
+ * @description URL forward based on unvalidated user-input
+ * may cause file information disclosure or
+ * redirection to malicious web sites.
* @kind path-problem
* @problem.severity error
+ * @security-severity 6.1
* @precision high
- * @id java/unsafe-url-forward-include
+ * @id java/unvalidated-url-forward
* @tags security
- * external/cwe-552
+ * external/cwe/cwe-552
+ * external/cwe/cwe-601
*/
import java
@@ -16,5 +19,5 @@ import UnsafeUrlForwardFlow::PathGraph
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"
+select sink.getNode(), source, sink, "Untrusted URL forward depends on a $@.", source.getNode(),
+ "user-provided value"
From 09bc21dbd36e4f0e4ab954d5f691bc440b2cb0c5 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Fri, 1 Dec 2023 08:56:20 -0500
Subject: [PATCH 12/40] Java: rename 'UnsafeUrlForward' to 'UrlForward'
---
.../{UnsafeUrlForward.qll => UrlForward.qll} | 22 +++++++++----------
...rlForwardQuery.qll => UrlForwardQuery.qll} | 18 +++++++--------
...{UnsafeUrlForward.java => UrlForward.java} | 2 +-
...nsafeUrlForward.qhelp => UrlForward.qhelp} | 2 +-
.../{UnsafeUrlForward.ql => UrlForward.ql} | 8 +++----
.../security/CWE-552/UnsafeRequestPath.java | 2 +-
.../CWE-552/UnsafeServletRequestDispatch.java | 8 +++----
.../security/CWE-552/UnsafeUrlForwardTest.ql | 18 ---------------
...dTest.expected => UrlForwardTest.expected} | 0
...afeUrlForward.java => UrlForwardTest.java} | 16 +++++++-------
.../security/CWE-552/UrlForwardTest.ql | 18 +++++++++++++++
11 files changed, 57 insertions(+), 57 deletions(-)
rename java/ql/lib/semmle/code/java/security/{UnsafeUrlForward.qll => UrlForward.qll} (66%)
rename java/ql/lib/semmle/code/java/security/{UnsafeUrlForwardQuery.qll => UrlForwardQuery.qll} (55%)
rename java/ql/src/Security/CWE/CWE-552/{UnsafeUrlForward.java => UrlForward.java} (97%)
rename java/ql/src/Security/CWE/CWE-552/{UnsafeUrlForward.qhelp => UrlForward.qhelp} (98%)
rename java/ql/src/Security/CWE/CWE-552/{UnsafeUrlForward.ql => UrlForward.ql} (71%)
delete mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.ql
rename java/ql/test/query-tests/security/CWE-552/{UnsafeUrlForwardTest.expected => UrlForwardTest.expected} (100%)
rename java/ql/test/query-tests/security/CWE-552/{UnsafeUrlForward.java => UrlForwardTest.java} (83%)
create mode 100644 java/ql/test/query-tests/security/CWE-552/UrlForwardTest.ql
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
similarity index 66%
rename from java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
rename to java/ql/lib/semmle/code/java/security/UrlForward.qll
index 4a529896f866..aa21ed48aba8 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -1,23 +1,23 @@
-/** Provides classes related to unsafe URL forwarding in Java. */
+/** Provides classes to reason about URL forward attacks. */
import java
private import semmle.code.java.dataflow.ExternalFlow
private import semmle.code.java.dataflow.FlowSources
private import semmle.code.java.dataflow.StringPrefixes
-/** A sink for unsafe URL forward vulnerabilities. */
-abstract class UnsafeUrlForwardSink extends DataFlow::Node { }
+/** A URL forward sink. */
+abstract class UrlForwardSink extends DataFlow::Node { }
-/** A default sink representing methods susceptible to unsafe URL forwarding. */
-private class DefaultUnsafeUrlForwardSink extends UnsafeUrlForwardSink {
- DefaultUnsafeUrlForwardSink() { sinkNode(this, "url-forward") }
+/** 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 which
* is reachable from a Spring entry point.
*/
-private class SpringUrlForwardSink extends UnsafeUrlForwardSink {
+private class SpringUrlForwardSink extends UrlForwardSink {
SpringUrlForwardSink() {
// TODO: check if can use MaD "Annotated" for `SpringRequestMappingMethod` or if too complicated for MaD (probably too complicated).
any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable()) and
@@ -32,10 +32,10 @@ private class ForwardPrefix extends InterestingPrefix {
override int getOffset() { result = 0 }
}
-/** A sanitizer for unsafe URL forward vulnerabilities. */
-abstract class UnsafeUrlForwardSanitizer extends DataFlow::Node { }
+/** A URL forward sanitizer. */
+abstract class UrlForwardSanitizer extends DataFlow::Node { }
-private class PrimitiveSanitizer extends UnsafeUrlForwardSanitizer {
+private class PrimitiveSanitizer extends UrlForwardSanitizer {
PrimitiveSanitizer() {
this.getType() instanceof PrimitiveType or
this.getType() instanceof BoxedType or
@@ -44,7 +44,7 @@ private class PrimitiveSanitizer extends UnsafeUrlForwardSanitizer {
}
// TODO: double-check this sanitizer (and should I switch all "sanitizer" naming to "barrier" instead?)
-private class FollowsSanitizingPrefix extends UnsafeUrlForwardSanitizer {
+private class FollowsSanitizingPrefix extends UrlForwardSanitizer {
FollowsSanitizingPrefix() { this.asExpr() = any(SanitizingPrefix fp).getAnAppendedExpression() }
}
diff --git a/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
similarity index 55%
rename from java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
rename to java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
index 496702345820..dadf4be612ff 100644
--- a/java/ql/lib/semmle/code/java/security/UnsafeUrlForwardQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
@@ -1,18 +1,18 @@
-/** Provides configurations to be used in queries related to unsafe URL forwarding. */
+/** Provides a taint-tracking configuration for reasoning about URL forwarding. */
import java
-import semmle.code.java.security.UnsafeUrlForward
+import semmle.code.java.security.UrlForward
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.security.PathSanitizer
/**
- * A taint-tracking configuration for untrusted user input in a URL forward or include.
+ * A taint-tracking configuration for reasoning about URL forwarding.
*/
-module UnsafeUrlForwardFlowConfig implements DataFlow::ConfigSig {
+module UrlForwardFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof ThreatModelFlowSource and
- // TODO: move below logic to class in UnsafeUrlForward.qll? And check exactly why these were excluded.
+ // TODO: move below logic to class in UrlForward.qll? And check exactly why these were excluded.
not exists(MethodCall ma, Method m | ma.getMethod() = m |
(
m instanceof HttpServletRequestGetRequestUriMethod or
@@ -23,10 +23,10 @@ module UnsafeUrlForwardFlowConfig implements DataFlow::ConfigSig {
)
}
- predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeUrlForwardSink }
+ predicate isSink(DataFlow::Node sink) { sink instanceof UrlForwardSink }
predicate isBarrier(DataFlow::Node node) {
- node instanceof UnsafeUrlForwardSanitizer or
+ node instanceof UrlForwardSanitizer or
node instanceof PathInjectionSanitizer
}
@@ -35,6 +35,6 @@ module UnsafeUrlForwardFlowConfig implements DataFlow::ConfigSig {
}
/**
- * Taint-tracking flow for untrusted user input in a URL forward or include.
+ * Taint-tracking flow for URL forwarding.
*/
-module UnsafeUrlForwardFlow = TaintTracking::Global;
+module UrlForwardFlow = TaintTracking::Global;
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.java b/java/ql/src/Security/CWE/CWE-552/UrlForward.java
similarity index 97%
rename from java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.java
rename to java/ql/src/Security/CWE/CWE-552/UrlForward.java
index d159c4057362..53b959bb8896 100644
--- a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.java
+++ b/java/ql/src/Security/CWE/CWE-552/UrlForward.java
@@ -7,7 +7,7 @@
import org.springframework.web.servlet.ModelAndView;
@Controller
-public class UnsafeUrlForward {
+public class UrlForward {
@GetMapping("/bad1")
public ModelAndView bad1(String url) {
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qhelp b/java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp
similarity index 98%
rename from java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qhelp
rename to java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp
index 2e425952edc3..fa9ffd45c103 100644
--- a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.qhelp
+++ b/java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp
@@ -27,7 +27,7 @@ without validating the input, which may cause file leakage. In the good1
ordinary forwarding requests are shown, which will not cause file leakage.
-
+
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
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql b/java/ql/src/Security/CWE/CWE-552/UrlForward.ql
similarity index 71%
rename from java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
rename to java/ql/src/Security/CWE/CWE-552/UrlForward.ql
index 06686d5e0bd0..66d3d0dd1ca3 100644
--- a/java/ql/src/Security/CWE/CWE-552/UnsafeUrlForward.ql
+++ b/java/ql/src/Security/CWE/CWE-552/UrlForward.ql
@@ -14,10 +14,10 @@
*/
import java
-import semmle.code.java.security.UnsafeUrlForwardQuery
-import UnsafeUrlForwardFlow::PathGraph
+import semmle.code.java.security.UrlForwardQuery
+import UrlForwardFlow::PathGraph
-from UnsafeUrlForwardFlow::PathNode source, UnsafeUrlForwardFlow::PathNode sink
-where UnsafeUrlForwardFlow::flowPath(source, sink)
+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"
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java b/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
index 55afe84bc192..3d82e7d783ed 100644
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
@@ -20,7 +20,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
String path = ((HttpServletRequest) request).getServletPath();
// A sample payload "/%57EB-INF/web.xml" can bypass this `startsWith` check
if (path != null && !path.startsWith("/WEB-INF")) {
- request.getRequestDispatcher(path).forward(request, response); // $ hasUnsafeUrlForward
+ request.getRequestDispatcher(path).forward(request, response); // $ hasUrlForward
} else {
chain.doFilter(request, response);
}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java b/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
index 9d501f2ec0df..66521c5897b0 100644
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
+++ b/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
@@ -29,7 +29,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
rd.forward(request, response);
} else {
ServletContext sc = cfg.getServletContext();
- RequestDispatcher rd = sc.getRequestDispatcher(returnURL); // $ hasUnsafeUrlForward
+ RequestDispatcher rd = sc.getRequestDispatcher(returnURL); // $ hasUrlForward
rd.forward(request, response);
}
}
@@ -45,7 +45,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
rd.forward(request, response);
} else {
- RequestDispatcher rd = request.getRequestDispatcher(returnURL); // $ hasUnsafeUrlForward
+ RequestDispatcher rd = request.getRequestDispatcher(returnURL); // $ hasUrlForward
rd.forward(request, response);
}
}
@@ -73,7 +73,7 @@ protected void doHead2(HttpServletRequest request, HttpServletResponse response)
// A sample payload "/pages/welcome.jsp/../WEB-INF/web.xml" can bypass the `startsWith` check
// The payload "/pages/welcome.jsp/../../%57EB-INF/web.xml" can bypass the check as well since RequestDispatcher will decode `%57` as `W`
if (path.startsWith(BASE_PATH)) {
- request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUnsafeUrlForward
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
}
}
@@ -110,7 +110,7 @@ protected void doHead5(HttpServletRequest request, HttpServletResponse response)
Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
if (!requestedPath.startsWith("/WEB-INF") && !requestedPath.startsWith("/META-INF")) {
- request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ MISSING: hasUnsafeUrlForward
+ request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ MISSING: hasUrlForward
}
}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.ql b/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.ql
deleted file mode 100644
index cf77edcf12a4..000000000000
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.ql
+++ /dev/null
@@ -1,18 +0,0 @@
-import java
-import TestUtilities.InlineExpectationsTest
-import semmle.code.java.security.UnsafeUrlForwardQuery
-
-module UnsafeUrlForwardTest implements TestSig {
- string getARelevantTag() { result = "hasUnsafeUrlForward" }
-
- predicate hasActualResult(Location location, string element, string tag, string value) {
- tag = "hasUnsafeUrlForward" and
- exists(UnsafeUrlForwardFlow::PathNode sink | UnsafeUrlForwardFlow::flowPath(_, sink) |
- location = sink.getNode().getLocation() and
- element = sink.getNode().toString() and
- value = ""
- )
- }
-}
-
-import MakeTest
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.expected b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.expected
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-552/UnsafeUrlForwardTest.expected
rename to java/ql/test/query-tests/security/CWE-552/UrlForwardTest.expected
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
similarity index 83%
rename from java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java
rename to java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
index 0a00637cd44c..2db1e812fb6b 100644
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeUrlForward.java
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
@@ -7,35 +7,35 @@
import org.springframework.web.servlet.ModelAndView;
@Controller
-public class UnsafeUrlForward {
+public class UrlForwardTest {
@GetMapping("/bad1")
public ModelAndView bad1(String url) {
- return new ModelAndView(url); // $ hasUnsafeUrlForward
+ return new ModelAndView(url); // $ hasUrlForward
}
@GetMapping("/bad2")
public ModelAndView bad2(String url) {
ModelAndView modelAndView = new ModelAndView();
- modelAndView.setViewName(url); // $ hasUnsafeUrlForward
+ modelAndView.setViewName(url); // $ hasUrlForward
return modelAndView;
}
@GetMapping("/bad3")
public String bad3(String url) {
- return "forward:" + url + "/swagger-ui/index.html"; // $ hasUnsafeUrlForward
+ return "forward:" + url + "/swagger-ui/index.html"; // $ hasUrlForward
}
@GetMapping("/bad4")
public ModelAndView bad4(String url) {
- ModelAndView modelAndView = new ModelAndView("forward:" + url); // $ hasUnsafeUrlForward
+ ModelAndView modelAndView = new ModelAndView("forward:" + url); // $ hasUrlForward
return modelAndView;
}
@GetMapping("/bad5")
public void bad5(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher(url).include(request, response); // $ hasUnsafeUrlForward
+ request.getRequestDispatcher(url).include(request, response); // $ hasUrlForward
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
@@ -46,7 +46,7 @@ public void bad5(String url, HttpServletRequest request, HttpServletResponse res
@GetMapping("/bad6")
public void bad6(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").include(request, response); // $ hasUnsafeUrlForward
+ request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").include(request, response); // $ hasUrlForward
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
@@ -57,7 +57,7 @@ public void bad6(String url, HttpServletRequest request, HttpServletResponse res
@GetMapping("/bad7")
public void bad7(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").forward(request, response); // $ hasUnsafeUrlForward
+ request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").forward(request, response); // $ hasUrlForward
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.ql b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.ql
new file mode 100644
index 000000000000..4e62a35752bb
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.ql
@@ -0,0 +1,18 @@
+import java
+import TestUtilities.InlineExpectationsTest
+import semmle.code.java.security.UrlForwardQuery
+
+module UrlForwardTest implements TestSig {
+ string getARelevantTag() { result = "hasUrlForward" }
+
+ predicate hasActualResult(Location location, string element, string tag, string value) {
+ tag = "hasUrlForward" and
+ exists(UrlForwardFlow::PathNode sink | UrlForwardFlow::flowPath(_, sink) |
+ location = sink.getNode().getLocation() and
+ element = sink.getNode().toString() and
+ value = ""
+ )
+ }
+}
+
+import MakeTest
From c331393cfd3982f06098e7f1a0613124c0cb44f4 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Fri, 1 Dec 2023 12:14:27 -0500
Subject: [PATCH 13/40] Java: update qhelp
---
.../CWE/CWE-552/UnsafeLoadSpringResource.java | 21 -------
.../CWE/CWE-552/UnsafeResourceGet.java | 18 ------
.../CWE-552/UnsafeServletRequestDispatch.java | 11 ----
.../src/Security/CWE/CWE-552/UrlForward.java | 43 ++++----------
.../src/Security/CWE/CWE-552/UrlForward.qhelp | 58 ++++---------------
.../ql/src/Security/CWE/CWE-552/UrlForward.ql | 6 +-
6 files changed, 25 insertions(+), 132 deletions(-)
delete mode 100644 java/ql/src/Security/CWE/CWE-552/UnsafeLoadSpringResource.java
delete mode 100644 java/ql/src/Security/CWE/CWE-552/UnsafeResourceGet.java
delete mode 100644 java/ql/src/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeLoadSpringResource.java b/java/ql/src/Security/CWE/CWE-552/UnsafeLoadSpringResource.java
deleted file mode 100644
index ce462fe490ef..000000000000
--- a/java/ql/src/Security/CWE/CWE-552/UnsafeLoadSpringResource.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeResourceGet.java b/java/ql/src/Security/CWE/CWE-552/UnsafeResourceGet.java
deleted file mode 100644
index 8b3583bf59e2..000000000000
--- a/java/ql/src/Security/CWE/CWE-552/UnsafeResourceGet.java
+++ /dev/null
@@ -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());
-}
diff --git a/java/ql/src/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java b/java/ql/src/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java
deleted file mode 100644
index a2bbf3dfcd85..000000000000
--- a/java/ql/src/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java
+++ /dev/null
@@ -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("%")) { ... }
diff --git a/java/ql/src/Security/CWE/CWE-552/UrlForward.java b/java/ql/src/Security/CWE/CWE-552/UrlForward.java
index 53b959bb8896..db701fbcd9a8 100644
--- a/java/ql/src/Security/CWE/CWE-552/UrlForward.java
+++ b/java/ql/src/Security/CWE/CWE-552/UrlForward.java
@@ -1,38 +1,17 @@
-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;
+public class UrlForward extends HttpServlet {
+ private static final String VALID_FORWARD = "https://cwe.mitre.org/data/definitions/552.html";
-@Controller
-public class UrlForward {
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ ServletConfig cfg = getServletConfig();
+ ServletContext sc = cfg.getServletContext();
- @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();
- }
- }
+ // BAD: a request parameter is incorporated without validation into a URL forward
+ sc.getRequestDispatcher(request.getParameter("target")).forward(request, response);
- @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();
+ // 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);
}
}
}
diff --git a/java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp b/java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp
index fa9ffd45c103..2b06a851a2b6 100644
--- a/java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp
+++ b/java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp
@@ -5,66 +5,32 @@
-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.
+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.
-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 user_content/) or disallowed
-(e.g. must not come within /internal) paths, ensuring that neither path traversal using ../
-or URL encoding are used to evade these checks.
-
+To guard against untrusted URL forwarding, it is advisable to avoid putting user input
+directly into a forwarded URL. Instead, maintain a list of authorized
+URLs on the server; then choose from that list based on the user input provided.
-The following examples show the bad case and the good case respectively.
-The bad methods show an HTTP request parameter being used directly in a URL forward
-without validating the input, which may cause file leakage. In the good1 method,
-ordinary forwarding requests are shown, which will not cause file leakage.
+
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.
-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.
-
-
-
-
-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.
-
-
-
-
-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.
-
-
-
-File Disclosure:
- Unsafe Url Forward.
-
-Jakarta Javadoc:
- Security vulnerability with unsafe usage of RequestDispatcher.
-
-Micro Focus:
- File Disclosure: J2EE
-
-CVE-2015-5174:
- Apache Tomcat 6.0/7.0/8.0/9.0 Servletcontext getResource/getResourceAsStream/getResourcePaths Path Traversal
-
-CVE-2019-3799:
- CVE-2019-3799 - Spring-Cloud-Config-Server Directory Traversal < 2.1.2, 2.0.4, 1.4.6
+
+OWASP:
+ Unvalidated Redirects and Forwards Cheat Sheet.
+
diff --git a/java/ql/src/Security/CWE/CWE-552/UrlForward.ql b/java/ql/src/Security/CWE/CWE-552/UrlForward.ql
index 66d3d0dd1ca3..95c540049a21 100644
--- a/java/ql/src/Security/CWE/CWE-552/UrlForward.ql
+++ b/java/ql/src/Security/CWE/CWE-552/UrlForward.ql
@@ -1,16 +1,14 @@
/**
* @name URL forward from a remote source
* @description URL forward based on unvalidated user-input
- * may cause file information disclosure or
- * redirection to malicious web sites.
+ * may cause file information disclosure.
* @kind path-problem
* @problem.severity error
- * @security-severity 6.1
+ * @security-severity 7.5
* @precision high
* @id java/unvalidated-url-forward
* @tags security
* external/cwe/cwe-552
- * external/cwe/cwe-601
*/
import java
From 5fa63ab5c22adc24b62cdfdc4efe12e578ede3bf Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 4 Dec 2023 10:31:47 -0500
Subject: [PATCH 14/40] Java: update/add some TODO comments
---
.../semmle/code/java/security/UrlForward.qll | 31 ++++++++++---------
.../code/java/security/UrlForwardQuery.qll | 9 +++---
2 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index aa21ed48aba8..7e68f07987b1 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -8,48 +8,49 @@ private import semmle.code.java.dataflow.StringPrefixes
/** A URL forward sink. */
abstract class UrlForwardSink extends DataFlow::Node { }
-/** A default sink representing methods susceptible to URL forwarding attacks. */
+/**
+ * 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 which
- * is reachable from a Spring entry point.
+ * An expression appended (perhaps indirectly) to `"forward:"`
+ * and reachable from a Spring entry point.
*/
private class SpringUrlForwardSink extends UrlForwardSink {
SpringUrlForwardSink() {
- // TODO: check if can use MaD "Annotated" for `SpringRequestMappingMethod` or if too complicated for MaD (probably too complicated).
- any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable()) and
+ any(SpringRequestMappingMethod srmm).polyCalls*(this.getEnclosingCallable()) and
this.asExpr() = any(ForwardPrefix fp).getAnAppendedExpression()
}
}
-// TODO: should this potentially be "include:" as well? Or does that not work similarly?
private class ForwardPrefix extends InterestingPrefix {
ForwardPrefix() { this.getStringValue() = "forward:" }
override int getOffset() { result = 0 }
}
-/** A URL forward sanitizer. */
-abstract class UrlForwardSanitizer extends DataFlow::Node { }
+/** A URL forward barrier. */
+abstract class UrlForwardBarrier extends DataFlow::Node { }
-private class PrimitiveSanitizer extends UrlForwardSanitizer {
- PrimitiveSanitizer() {
+private class PrimitiveBarrier extends UrlForwardBarrier {
+ PrimitiveBarrier() {
this.getType() instanceof PrimitiveType or
this.getType() instanceof BoxedType or
this.getType() instanceof NumberType
}
}
-// TODO: double-check this sanitizer (and should I switch all "sanitizer" naming to "barrier" instead?)
-private class FollowsSanitizingPrefix extends UrlForwardSanitizer {
- FollowsSanitizingPrefix() { this.asExpr() = any(SanitizingPrefix fp).getAnAppendedExpression() }
+private class FollowsBarrierPrefix extends UrlForwardBarrier {
+ FollowsBarrierPrefix() { this.asExpr() = any(BarrierPrefix fp).getAnAppendedExpression() }
}
-private class SanitizingPrefix extends InterestingPrefix {
- SanitizingPrefix() {
+private class BarrierPrefix extends InterestingPrefix {
+ BarrierPrefix() {
+ // TODO: why not META-INF here as well? (and are `/` correct?)
not this.getStringValue().matches("/WEB-INF/%") and
not this.getStringValue() = "forward:"
}
diff --git a/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
index dadf4be612ff..c92467490f30 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
@@ -12,25 +12,24 @@ import semmle.code.java.security.PathSanitizer
module UrlForwardFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof ThreatModelFlowSource and
- // TODO: move below logic to class in UrlForward.qll? And check exactly why these were excluded.
- not exists(MethodCall ma, Method m | ma.getMethod() = m |
+ // excluded due to FPs
+ not exists(MethodCall mc, Method m | mc.getMethod() = m |
(
m instanceof HttpServletRequestGetRequestUriMethod or
m instanceof HttpServletRequestGetRequestUrlMethod or
m instanceof HttpServletRequestGetPathMethod
) and
- ma = source.asExpr()
+ mc = source.asExpr()
)
}
predicate isSink(DataFlow::Node sink) { sink instanceof UrlForwardSink }
predicate isBarrier(DataFlow::Node node) {
- node instanceof UrlForwardSanitizer or
+ node instanceof UrlForwardBarrier or
node instanceof PathInjectionSanitizer
}
- // TODO: check if the below is still needed after removing path-injection related sinks.
DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
}
From e75c96c0f9be67d4423446177dedc808a1ec9239 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 10 Dec 2023 22:03:11 -0500
Subject: [PATCH 15/40] Java: combine test cases; add test for
StaplerResponse.forward
---
.../security/CWE-552/UnsafeRequestPath.java | 52 -----
.../CWE-552/UnsafeServletRequestDispatch.java | 131 -------------
.../security/CWE-552/UrlForwardTest.java | 180 +++++++++++++++++-
.../test/query-tests/security/CWE-552/options | 2 +-
4 files changed, 180 insertions(+), 185 deletions(-)
delete mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
delete mode 100644 java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java b/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
deleted file mode 100644
index 3d82e7d783ed..000000000000
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeRequestPath.java
+++ /dev/null
@@ -1,52 +0,0 @@
-import java.io.IOException;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-// @WebFilter("/*")
-public class UnsafeRequestPath implements Filter {
- private static final String BASE_PATH = "/pages";
-
- @Override
- // BAD: Request dispatcher from servlet path without check
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- String path = ((HttpServletRequest) request).getServletPath();
- // A sample payload "/%57EB-INF/web.xml" can bypass this `startsWith` check
- if (path != null && !path.startsWith("/WEB-INF")) {
- request.getRequestDispatcher(path).forward(request, response); // $ hasUrlForward
- } else {
- chain.doFilter(request, response);
- }
- }
-
- // GOOD: Request dispatcher from servlet path with check
- public void doFilter2(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- String path = ((HttpServletRequest) request).getServletPath();
-
- if (path.startsWith(BASE_PATH) && !path.contains("..")) {
- request.getRequestDispatcher(path).forward(request, response);
- } else {
- chain.doFilter(request, response);
- }
- }
-
- // GOOD: Request dispatcher from servlet path with whitelisted string comparison
- public void doFilter3(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- String path = ((HttpServletRequest) request).getServletPath();
-
- if (path.equals("/comaction")) {
- request.getRequestDispatcher(path).forward(request, response);
- } else {
- chain.doFilter(request, response);
- }
- }
-}
diff --git a/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java b/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
deleted file mode 100644
index 66521c5897b0..000000000000
--- a/java/ql/test/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
+++ /dev/null
@@ -1,131 +0,0 @@
-import java.io.IOException;
-import java.net.URLDecoder;
-import java.io.File;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.RequestDispatcher;
-import javax.servlet.ServletException;
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletContext;
-
-public class UnsafeServletRequestDispatch extends HttpServlet {
- private static final String BASE_PATH = "/pages";
-
- @Override
- // BAD: Request dispatcher constructed from `ServletContext` without input validation
- protected void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String action = request.getParameter("action");
- String returnURL = request.getParameter("returnURL");
-
- ServletConfig cfg = getServletConfig();
- if (action.equals("Login")) {
- ServletContext sc = cfg.getServletContext();
- RequestDispatcher rd = sc.getRequestDispatcher("/Login.jsp");
- rd.forward(request, response);
- } else {
- ServletContext sc = cfg.getServletContext();
- RequestDispatcher rd = sc.getRequestDispatcher(returnURL); // $ hasUrlForward
- rd.forward(request, response);
- }
- }
-
- @Override
- // BAD: Request dispatcher constructed from `HttpServletRequest` without input validation
- protected void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String action = request.getParameter("action");
- String returnURL = request.getParameter("returnURL");
-
- if (action.equals("Login")) {
- RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
- rd.forward(request, response);
- } else {
- RequestDispatcher rd = request.getRequestDispatcher(returnURL); // $ hasUrlForward
- rd.forward(request, response);
- }
- }
-
- @Override
- // GOOD: Request dispatcher with a whitelisted URI
- protected void doPut(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String action = request.getParameter("action");
-
- if (action.equals("Login")) {
- RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
- rd.forward(request, response);
- } else if (action.equals("Register")) {
- RequestDispatcher rd = request.getRequestDispatcher("/Register.jsp");
- rd.forward(request, response);
- }
- }
-
- // BAD: Request dispatcher without path traversal check
- protected void doHead2(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String path = request.getParameter("path");
-
- // A sample payload "/pages/welcome.jsp/../WEB-INF/web.xml" can bypass the `startsWith` check
- // The payload "/pages/welcome.jsp/../../%57EB-INF/web.xml" can bypass the check as well since RequestDispatcher will decode `%57` as `W`
- if (path.startsWith(BASE_PATH)) {
- request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
- }
- }
-
- // GOOD: Request dispatcher with path traversal check
- protected void doHead3(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String path = request.getParameter("path");
-
- if (path.startsWith(BASE_PATH) && !path.contains("..")) {
- request.getServletContext().getRequestDispatcher(path).include(request, response);
- }
- }
-
- // GOOD: Request dispatcher with path normalization and comparison
- protected void doHead4(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String path = request.getParameter("path");
- Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
-
- // /pages/welcome.jsp/../../WEB-INF/web.xml becomes /WEB-INF/web.xml
- // /pages/welcome.jsp/../../%57EB-INF/web.xml becomes /%57EB-INF/web.xml
- if (requestedPath.startsWith(BASE_PATH)) {
- request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response);
- }
- }
-
- // FN: Request dispatcher with negation check and path normalization, but without URL decoding
- // When promoting this query, consider using FlowStates to make `getRequestDispatcher` a sink
- // only if a URL-decoding step has NOT been crossed (i.e. make URLDecoder.decode change the
- // state to a different value than the one required at the sink).
- protected void doHead5(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String path = request.getParameter("path");
- Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
-
- if (!requestedPath.startsWith("/WEB-INF") && !requestedPath.startsWith("/META-INF")) {
- request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ MISSING: hasUrlForward
- }
- }
-
- // GOOD: Request dispatcher with path traversal check and URL decoding
- protected void doHead6(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String path = request.getParameter("path");
- boolean hasEncoding = path.contains("%");
- while (hasEncoding) {
- path = URLDecoder.decode(path, "UTF-8");
- hasEncoding = path.contains("%");
- }
-
- if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- request.getServletContext().getRequestDispatcher(path).include(request, response);
- }
- }
-}
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
index 2db1e812fb6b..e41bc65848eb 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
@@ -1,14 +1,29 @@
import java.io.IOException;
+import java.net.URLDecoder;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServlet;
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;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
@Controller
-public class UrlForwardTest {
+public class UrlForwardTest extends HttpServlet implements Filter {
+ // (1) ORIGINAL
@GetMapping("/bad1")
public ModelAndView bad1(String url) {
return new ModelAndView(url); // $ hasUrlForward
@@ -75,4 +90,167 @@ public void good1(String url, HttpServletRequest request, HttpServletResponse re
e.printStackTrace();
}
}
+
+ // (2) UnsafeRequestPath
+ private static final String BASE_PATH = "/pages";
+
+ @Override
+ // BAD: Request dispatcher from servlet path without check
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ String path = ((HttpServletRequest) request).getServletPath();
+ // A sample payload "/%57EB-INF/web.xml" can bypass this `startsWith` check
+ if (path != null && !path.startsWith("/WEB-INF")) {
+ request.getRequestDispatcher(path).forward(request, response); // $ hasUrlForward
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+
+ // GOOD: Request dispatcher from servlet path with check
+ public void doFilter2(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ String path = ((HttpServletRequest) request).getServletPath();
+
+ if (path.startsWith(BASE_PATH) && !path.contains("..")) {
+ request.getRequestDispatcher(path).forward(request, response);
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+
+ // GOOD: Request dispatcher from servlet path with whitelisted string comparison
+ public void doFilter3(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ String path = ((HttpServletRequest) request).getServletPath();
+
+ if (path.equals("/comaction")) {
+ request.getRequestDispatcher(path).forward(request, response);
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+
+ // (3) UnsafeServletRequestDispatch
+ @Override
+ // BAD: Request dispatcher constructed from `ServletContext` without input validation
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String action = request.getParameter("action");
+ String returnURL = request.getParameter("returnURL");
+
+ ServletConfig cfg = getServletConfig();
+ if (action.equals("Login")) {
+ ServletContext sc = cfg.getServletContext();
+ RequestDispatcher rd = sc.getRequestDispatcher("/Login.jsp");
+ rd.forward(request, response);
+ } else {
+ ServletContext sc = cfg.getServletContext();
+ RequestDispatcher rd = sc.getRequestDispatcher(returnURL); // $ hasUrlForward
+ rd.forward(request, response);
+ }
+ }
+
+ @Override
+ // BAD: Request dispatcher constructed from `HttpServletRequest` without input validation
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String action = request.getParameter("action");
+ String returnURL = request.getParameter("returnURL");
+
+ if (action.equals("Login")) {
+ RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
+ rd.forward(request, response);
+ } else {
+ RequestDispatcher rd = request.getRequestDispatcher(returnURL); // $ hasUrlForward
+ rd.forward(request, response);
+ }
+ }
+
+ @Override
+ // GOOD: Request dispatcher with a whitelisted URI
+ protected void doPut(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String action = request.getParameter("action");
+
+ if (action.equals("Login")) {
+ RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
+ rd.forward(request, response);
+ } else if (action.equals("Register")) {
+ RequestDispatcher rd = request.getRequestDispatcher("/Register.jsp");
+ rd.forward(request, response);
+ }
+ }
+
+ // BAD: Request dispatcher without path traversal check
+ protected void doHead2(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+
+ // A sample payload "/pages/welcome.jsp/../WEB-INF/web.xml" can bypass the `startsWith` check
+ // The payload "/pages/welcome.jsp/../../%57EB-INF/web.xml" can bypass the check as well since RequestDispatcher will decode `%57` as `W`
+ if (path.startsWith(BASE_PATH)) {
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
+ }
+ }
+
+ // GOOD: Request dispatcher with path traversal check
+ protected void doHead3(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+
+ if (path.startsWith(BASE_PATH) && !path.contains("..")) {
+ request.getServletContext().getRequestDispatcher(path).include(request, response);
+ }
+ }
+
+ // GOOD: Request dispatcher with path normalization and comparison
+ protected void doHead4(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+ Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
+
+ // /pages/welcome.jsp/../../WEB-INF/web.xml becomes /WEB-INF/web.xml
+ // /pages/welcome.jsp/../../%57EB-INF/web.xml becomes /%57EB-INF/web.xml
+ if (requestedPath.startsWith(BASE_PATH)) {
+ request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response);
+ }
+ }
+
+ // FN: Request dispatcher with negation check and path normalization, but without URL decoding
+ // When promoting this query, consider using FlowStates to make `getRequestDispatcher` a sink
+ // only if a URL-decoding step has NOT been crossed (i.e. make URLDecoder.decode change the
+ // state to a different value than the one required at the sink).
+ // TODO: but does this need to take into account URLDecoder.decode in a loop...?
+ protected void doHead5(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+ Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
+
+ if (!requestedPath.startsWith("/WEB-INF") && !requestedPath.startsWith("/META-INF")) {
+ request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ MISSING: hasUrlForward
+ }
+ }
+
+ // GOOD: Request dispatcher with path traversal check and URL decoding
+ protected void doHead6(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+ boolean hasEncoding = path.contains("%");
+ while (hasEncoding) {
+ path = URLDecoder.decode(path, "UTF-8");
+ hasEncoding = path.contains("%");
+ }
+
+ if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ request.getServletContext().getRequestDispatcher(path).include(request, response);
+ }
+ }
+
+ // New Tests (i.e. Added by me)
+ public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object obj) throws IOException, ServletException {
+ String url = req.getParameter("target");
+ rsp.forward(obj, url, req); // $ hasUrlForward
+ }
+
}
diff --git a/java/ql/test/query-tests/security/CWE-552/options b/java/ql/test/query-tests/security/CWE-552/options
index 025b888db025..bda9516fb580 100644
--- a/java/ql/test/query-tests/security/CWE-552/options
+++ b/java/ql/test/query-tests/security/CWE-552/options
@@ -1 +1 @@
-//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/springframework-5.3.8/:${testdir}/../../../stubs/javax-faces-2.3/:${testdir}/../../../stubs/undertow-io-2.2/:${testdir}/../../../stubs/jboss-vfs-3.2/:${testdir}/../../../stubs/springframework-5.3.8/
+//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/springframework-5.3.8/:${testdir}/../../../stubs/javax-faces-2.3/:${testdir}/../../../stubs/undertow-io-2.2/:${testdir}/../../../stubs/jboss-vfs-3.2/:${testdir}/../../../stubs/stapler-1.263/:${testdir}/../../../stubs/apache-commons-fileupload-1.4/:${testdir}/../../../stubs/apache-commons-beanutils/:${testdir}/../../../stubs/saxon-xqj-9.x/:${testdir}/../../../stubs/apache-commons-lang/:${testdir}/../../../stubs/javax-servlet-2.5/
From c8ec301793067bb1d9d6c9f5e9ccacc59d086f01 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 10 Dec 2023 22:12:08 -0500
Subject: [PATCH 16/40] Java: add change note
---
java/ql/src/change-notes/2023-12-12-url-forward-query.md | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 java/ql/src/change-notes/2023-12-12-url-forward-query.md
diff --git a/java/ql/src/change-notes/2023-12-12-url-forward-query.md b/java/ql/src/change-notes/2023-12-12-url-forward-query.md
new file mode 100644
index 000000000000..4efc4b7c4e0a
--- /dev/null
+++ b/java/ql/src/change-notes/2023-12-12-url-forward-query.md
@@ -0,0 +1,4 @@
+---
+category: newQuery
+---
+* The query `java/unsafe-url-forward-dispatch-load` has been promoted from experimental to the main query pack. 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).
From 911a61df2234dc5f9519e3238159f2e7f53f1d3f Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 5 Mar 2024 11:08:24 -0500
Subject: [PATCH 17/40] Java: initial update of barrier and test cases to
remove FN
---
.../code/java/security/PathSanitizer.qll | 7 +-
.../semmle/code/java/security/UrlForward.qll | 110 +++++++++++++-
.../code/java/security/UrlForwardQuery.qll | 5 +-
.../security/CWE-552/UrlForwardTest.java | 135 ++++++++++++++++--
4 files changed, 237 insertions(+), 20 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/PathSanitizer.qll b/java/ql/lib/semmle/code/java/security/PathSanitizer.qll
index 4ca08f5becc6..f3c54629efde 100644
--- a/java/ql/lib/semmle/code/java/security/PathSanitizer.qll
+++ b/java/ql/lib/semmle/code/java/security/PathSanitizer.qll
@@ -64,7 +64,8 @@ private predicate exactPathMatchGuard(Guard g, Expr e, boolean branch) {
)
}
-private class ExactPathMatchSanitizer extends PathInjectionSanitizer {
+// TODO: switch back to private if possible
+class ExactPathMatchSanitizer extends PathInjectionSanitizer {
ExactPathMatchSanitizer() {
this = DataFlow::BarrierGuard::getABarrierNode()
or
@@ -151,7 +152,8 @@ private class DotDotCheckSanitizer extends PathInjectionSanitizer {
}
}
-private class BlockListGuard extends PathGuard instanceof MethodCall {
+// TODO: switch back to private if possible
+class BlockListGuard extends PathGuard instanceof MethodCall {
BlockListGuard() {
(isStringPartialMatch(this) or isPathPrefixMatch(this)) and
isDisallowedWord(super.getAnArgument())
@@ -228,6 +230,7 @@ private predicate isStringPartialMatch(MethodCall ma) {
exists(RefType t | t = ma.getMethod().getDeclaringType() |
t instanceof TypeString or t instanceof StringsKt
) and
+ // TODO ! Why not use `StringPartialMatchMethod` for the below?
getSourceMethod(ma.getMethod())
.hasName(["contains", "matches", "regionMatches", "indexOf", "lastIndexOf"])
}
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index 7e68f07987b1..073507fe33a6 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -4,6 +4,9 @@ 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.dataflow.DataFlow
+private import semmle.code.java.controlflow.Guards
/** A URL forward sink. */
abstract class UrlForwardSink extends DataFlow::Node { }
@@ -44,16 +47,121 @@ private class PrimitiveBarrier extends UrlForwardBarrier {
}
}
+// TODO: should this also take URL encoding/decoding into account?
+// TODO: and PathSanitization in general?
private class FollowsBarrierPrefix extends UrlForwardBarrier {
FollowsBarrierPrefix() { this.asExpr() = any(BarrierPrefix fp).getAnAppendedExpression() }
}
private class BarrierPrefix extends InterestingPrefix {
BarrierPrefix() {
- // TODO: why not META-INF here as well? (and are `/` correct?)
not this.getStringValue().matches("/WEB-INF/%") and
not this.getStringValue() = "forward:"
}
override int getOffset() { result = 0 }
}
+
+private class UrlPathBarrier extends UrlForwardBarrier {
+ UrlPathBarrier() {
+ this instanceof PathInjectionSanitizer and
+ (
+ this instanceof ExactPathMatchSanitizer //TODO: still need a better solution for this edge case...
+ or
+ // TODO: these don't enforce order of checks and PathSanitization... make bypass test cases.
+ this instanceof NoEncodingBarrier
+ or
+ this instanceof FullyDecodesBarrier
+ )
+ }
+}
+
+abstract class UrlDecodeCall extends MethodCall { }
+
+private class DefaultUrlDecodeCall extends UrlDecodeCall {
+ DefaultUrlDecodeCall() {
+ this.getMethod().hasQualifiedName("java.net", "URLDecoder", "decode") or // TODO: reuse existing class? Or make this a class?
+ this.getMethod().hasQualifiedName("org.eclipse.jetty.util.URIUtil", "URIUtil", "decodePath")
+ }
+}
+
+// TODO: this can probably be named/designed better...
+abstract class RepeatedStmt extends Stmt { }
+
+private class DefaultRepeatedStmt extends RepeatedStmt {
+ DefaultRepeatedStmt() { this instanceof LoopStmt }
+}
+
+abstract class CheckEncodingCall extends MethodCall { }
+
+private class DefaultCheckEncodingCall extends CheckEncodingCall {
+ DefaultCheckEncodingCall() {
+ // TODO: indexOf?, etc.
+ this.getMethod().hasQualifiedName("java.lang", "String", "contains") and // TODO: reuse existing class? Or make this a class?
+ this.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "%"
+ }
+}
+
+// TODO: better naming?
+// TODO: check if any URL decoding implementations _fully_ decode... or if all need to be called in a loop?
+// TODO: make this extendable instead of `RepeatedStmt`?
+private class RepeatedUrlDecodeCall extends MethodCall {
+ RepeatedUrlDecodeCall() {
+ this instanceof UrlDecodeCall and
+ this.getAnEnclosingStmt() instanceof RepeatedStmt
+ }
+}
+
+private class CheckEncodingGuard extends Guard instanceof MethodCall {
+ CheckEncodingGuard() { this instanceof CheckEncodingCall }
+
+ Expr getCheckedExpr() { result = this.(MethodCall).getQualifier() }
+}
+
+private predicate noEncodingGuard(Guard g, Expr e, boolean branch) {
+ g instanceof CheckEncodingGuard and
+ e = g.(CheckEncodingGuard).getCheckedExpr() and
+ branch = false
+ or
+ // branch = false and
+ // g instanceof AssignExpr and // AssignExpr
+ // exists(CheckEncodingCall call | g.(AssignExpr).getSource() = call | e = call.getQualifier())
+ branch = false and
+ g.(Expr).getType() instanceof BooleanType and // AssignExpr
+ (
+ exists(CheckEncodingCall call, AssignExpr ae |
+ ae.getSource() = call and
+ e = call.getQualifier() and
+ g = ae.getDest()
+ )
+ or
+ exists(CheckEncodingCall call, LocalVariableDeclExpr vde |
+ vde.getInitOrPatternSource() = call and
+ e = call.getQualifier() and
+ g = vde.getAnAccess() //and
+ //vde.getVariable() = g
+ )
+ )
+}
+
+// TODO: check edge case of !contains(%), make sure that behaves as expected at least.
+private class NoEncodingBarrier extends DataFlow::Node {
+ NoEncodingBarrier() { this = DataFlow::BarrierGuard::getABarrierNode() }
+}
+
+private predicate fullyDecodesGuard(Expr e) {
+ exists(CheckEncodingGuard g, RepeatedUrlDecodeCall decodeCall |
+ e = g.getCheckedExpr() and
+ g.controls(decodeCall.getBasicBlock(), true)
+ )
+}
+
+private class FullyDecodesBarrier extends DataFlow::Node {
+ FullyDecodesBarrier() {
+ exists(Variable v, Expr e | this.asExpr() = v.getAnAccess() |
+ fullyDecodesGuard(e) and
+ e = v.getAnAccess() and
+ e.getBasicBlock().bbDominates(this.asExpr().getBasicBlock())
+ )
+ }
+}
diff --git a/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
index c92467490f30..71d41f42deed 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
@@ -25,10 +25,7 @@ module UrlForwardFlowConfig implements DataFlow::ConfigSig {
predicate isSink(DataFlow::Node sink) { sink instanceof UrlForwardSink }
- predicate isBarrier(DataFlow::Node node) {
- node instanceof UrlForwardBarrier or
- node instanceof PathInjectionSanitizer
- }
+ predicate isBarrier(DataFlow::Node node) { node instanceof UrlForwardBarrier }
DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
}
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
index e41bc65848eb..c7a9d82b1a0a 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
@@ -112,8 +112,9 @@ public void doFilter2(ServletRequest request, ServletResponse response, FilterCh
throws IOException, ServletException {
String path = ((HttpServletRequest) request).getServletPath();
+ // actually BAD since could potentially bypass with ".." encoded as "%2e%2e"?
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
- request.getRequestDispatcher(path).forward(request, response);
+ request.getRequestDispatcher(path).forward(request, response); // $ hasUrlForward
} else {
chain.doFilter(request, response);
}
@@ -124,6 +125,7 @@ public void doFilter3(ServletRequest request, ServletResponse response, FilterCh
throws IOException, ServletException {
String path = ((HttpServletRequest) request).getServletPath();
+ // this is still good, should not flag here..., url-decoding first doesn't matter if looking for exact match... :(
if (path.equals("/comaction")) {
request.getRequestDispatcher(path).forward(request, response);
} else {
@@ -199,8 +201,9 @@ protected void doHead3(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
+ // actually BAD since could potentially bypass with ".." encoded as "%2e%2e"?
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
- request.getServletContext().getRequestDispatcher(path).include(request, response);
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
}
}
@@ -212,30 +215,68 @@ protected void doHead4(HttpServletRequest request, HttpServletResponse response)
// /pages/welcome.jsp/../../WEB-INF/web.xml becomes /WEB-INF/web.xml
// /pages/welcome.jsp/../../%57EB-INF/web.xml becomes /%57EB-INF/web.xml
+ // actually BAD since could potentially bypass with ".." encoded as "%2e%2e": "/pages/welcome.jsp/%2e%2e/%2e%2e/WEB-INF/web.xml" becomes /pages/welcome.jsp/%2e%2e/%2e%2e/WEB-INF/web.xml, which will pass this check and potentially be problematic if decoded later?
if (requestedPath.startsWith(BASE_PATH)) {
- request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response);
+ request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ hasUrlForward
}
}
- // FN: Request dispatcher with negation check and path normalization, but without URL decoding
- // When promoting this query, consider using FlowStates to make `getRequestDispatcher` a sink
- // only if a URL-decoding step has NOT been crossed (i.e. make URLDecoder.decode change the
- // state to a different value than the one required at the sink).
- // TODO: but does this need to take into account URLDecoder.decode in a loop...?
+ // BAD (original FN): Request dispatcher with negation check and path normalization, but without URL decoding
protected void doHead5(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
if (!requestedPath.startsWith("/WEB-INF") && !requestedPath.startsWith("/META-INF")) {
- request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ MISSING: hasUrlForward
+ request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ hasUrlForward
}
}
- // GOOD: Request dispatcher with path traversal check and URL decoding
- protected void doHead6(HttpServletRequest request, HttpServletResponse response)
+ // BAD (I added to test decode with no loop): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
+ protected void doHead7(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
+ path = URLDecoder.decode(path, "UTF-8");
+
+ if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
+ }
+ }
+
+ // GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
+ protected void doHead6(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path"); // v
+
+ if (path.contains("%")){ // v.getAnAccess()
+ while (path.contains("%")) {
+ path = URLDecoder.decode(path, "UTF-8");
+ }
+ }
+
+ if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ request.getServletContext().getRequestDispatcher(path).include(request, response);
+ }
+ }
+
+ // GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
+ protected void doHead8(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path"); // v
+ while (path.contains("%")) {
+ path = URLDecoder.decode(path, "UTF-8");
+ }
+
+ if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ request.getServletContext().getRequestDispatcher(path).include(request, response);
+ }
+ }
+
+ // FP now....
+ // GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
+ protected void doHead9(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path"); // v
boolean hasEncoding = path.contains("%");
while (hasEncoding) {
path = URLDecoder.decode(path, "UTF-8");
@@ -243,14 +284,82 @@ protected void doHead6(HttpServletRequest request, HttpServletResponse response)
}
if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- request.getServletContext().getRequestDispatcher(path).include(request, response);
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ SPURIOUS: hasUrlForward
}
}
- // New Tests (i.e. Added by me)
+ // New Tests
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object obj) throws IOException, ServletException {
String url = req.getParameter("target");
rsp.forward(obj, url, req); // $ hasUrlForward
}
+ // Other Tests for edge cases:
+ // // GOOD (I added): Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
+ // // testing `if` stmt requirement for BB controlling
+ // protected void doHead12(HttpServletRequest request, HttpServletResponse response)
+ // throws ServletException, IOException {
+ // String path = request.getParameter("path");
+ // if (path.contains("%")) {
+ // while (path.contains("%")) {
+ // path = URLDecoder.decode(path, "UTF-8");
+ // }
+ // }
+ // if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ // request.getServletContext().getRequestDispatcher(path).include(request, response);
+ // }
+ // }
+ // // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
+ // // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
+ // // having previously been checked against a block-list of forbidden values."
+ // protected void doHead8(HttpServletRequest request, HttpServletResponse response)
+ // throws ServletException, IOException {
+ // String path = request.getParameter("path");
+
+ // if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ // boolean hasEncoding = path.contains("%"); // BAD: doesn't do anything with the check...
+ // request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
+ // }
+ // }
+ // // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
+ // // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
+ // // having previously been checked against a block-list of forbidden values."
+ // protected void doHead9(HttpServletRequest request, HttpServletResponse response)
+ // throws ServletException, IOException {
+ // String path = request.getParameter("path");
+
+ // boolean hasEncoding = path.contains("%"); // BAD: doesn't do anything with the check... and check comes BEFORE blocklist so guard should not trigger
+ // if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ // request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
+ // }
+ // }
+
+ // // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
+ // // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
+ // // having previously been checked against a block-list of forbidden values."
+ // protected void doHead10(HttpServletRequest request, HttpServletResponse response)
+ // throws ServletException, IOException {
+ // String path = request.getParameter("path");
+
+ // if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ // if (path.contains("%")){ // BAD: wrong check
+ // request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
+ // }
+ // }
+ // }
+
+ // // "GOOD" (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
+ // // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
+ // // having previously been checked against a block-list of forbidden values."
+ // protected void doHead11(HttpServletRequest request, HttpServletResponse response)
+ // throws ServletException, IOException {
+ // String path = request.getParameter("path");
+
+ // if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ // if (!path.contains("%")){ // GOOD: right check
+ // request.getServletContext().getRequestDispatcher(path).include(request, response);
+ // }
+ // }
+ // }
+
}
From f573032b2e74ccf8165650b83602b08a8f2af126 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 5 Mar 2024 13:56:35 -0500
Subject: [PATCH 18/40] Java: remove todo comments from ext files
---
java/ql/lib/ext/jakarta.servlet.model.yml | 1 -
java/ql/lib/ext/javax.portlet.model.yml | 1 -
java/ql/lib/ext/javax.servlet.model.yml | 1 -
3 files changed, 3 deletions(-)
diff --git a/java/ql/lib/ext/jakarta.servlet.model.yml b/java/ql/lib/ext/jakarta.servlet.model.yml
index fc1274cadaf6..be2feeb3c375 100644
--- a/java/ql/lib/ext/jakarta.servlet.model.yml
+++ b/java/ql/lib/ext/jakarta.servlet.model.yml
@@ -3,6 +3,5 @@ extensions:
pack: codeql/java-all
extensible: sinkModel
data:
- # TODO: potentially switch to using Argument[this] of `RequestDispatcher.forward|include` as sink instead of the below.
- ["jakarta.servlet", "ServletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
- ["jakarta.servlet", "ServletRequest", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
diff --git a/java/ql/lib/ext/javax.portlet.model.yml b/java/ql/lib/ext/javax.portlet.model.yml
index e39484abcb74..15d108866247 100644
--- a/java/ql/lib/ext/javax.portlet.model.yml
+++ b/java/ql/lib/ext/javax.portlet.model.yml
@@ -3,5 +3,4 @@ extensions:
pack: codeql/java-all
extensible: sinkModel
data:
- # TODO: potentially switch to using Argument[this] of `PortletRequestDispatcher.forward|include` as sink instead of the below.
- ["javax.portlet", "PortletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
diff --git a/java/ql/lib/ext/javax.servlet.model.yml b/java/ql/lib/ext/javax.servlet.model.yml
index 7c405ac0de91..d27011c6e127 100644
--- a/java/ql/lib/ext/javax.servlet.model.yml
+++ b/java/ql/lib/ext/javax.servlet.model.yml
@@ -14,7 +14,6 @@ extensions:
extensible: sinkModel
data:
- ["javax.servlet", "ServletContext", True, "getResourceAsStream", "(String)", "", "Argument[0]", "path-injection", "ai-manual"]
- # TODO: potentially switch to using Argument[this] of `RequestDispatcher.forward|include` as sink instead of the below.
- ["javax.servlet", "ServletContext", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
- ["javax.servlet", "ServletRequest", True, "getRequestDispatcher", "(String)", "", "Argument[0]", "url-forward", "manual"]
- addsTo:
From 2708e53c7f3b54f1755596513aad1b444457ee9b Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 5 Mar 2024 14:02:41 -0500
Subject: [PATCH 19/40] Java: remove redundant imports
---
java/ql/lib/semmle/code/java/security/UrlForward.qll | 1 -
java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll | 1 -
2 files changed, 2 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index 073507fe33a6..79f8e5f2b288 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -5,7 +5,6 @@ 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.dataflow.DataFlow
private import semmle.code.java.controlflow.Guards
/** A URL forward sink. */
diff --git a/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
index 71d41f42deed..30de4ef8354b 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
@@ -3,7 +3,6 @@
import java
import semmle.code.java.security.UrlForward
import semmle.code.java.dataflow.FlowSources
-import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.security.PathSanitizer
/**
From 43b49628fc54cd2de38768e14ff9f22720852919 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 5 Mar 2024 14:36:43 -0500
Subject: [PATCH 20/40] Java: use new 'SimpleTypeSanitizer', and update some
non-extending subtype relationships
---
.../semmle/code/java/security/UrlForward.qll | 17 ++++-------------
1 file changed, 4 insertions(+), 13 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index 79f8e5f2b288..d19b8c163fdd 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -6,6 +6,7 @@ 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 { }
@@ -38,13 +39,7 @@ private class ForwardPrefix extends InterestingPrefix {
/** A URL forward barrier. */
abstract class UrlForwardBarrier extends DataFlow::Node { }
-private class PrimitiveBarrier extends UrlForwardBarrier {
- PrimitiveBarrier() {
- this.getType() instanceof PrimitiveType or
- this.getType() instanceof BoxedType or
- this.getType() instanceof NumberType
- }
-}
+private class PrimitiveBarrier extends UrlForwardBarrier instanceof SimpleTypeSanitizer { }
// TODO: should this also take URL encoding/decoding into account?
// TODO: and PathSanitization in general?
@@ -87,9 +82,7 @@ private class DefaultUrlDecodeCall extends UrlDecodeCall {
// TODO: this can probably be named/designed better...
abstract class RepeatedStmt extends Stmt { }
-private class DefaultRepeatedStmt extends RepeatedStmt {
- DefaultRepeatedStmt() { this instanceof LoopStmt }
-}
+private class DefaultRepeatedStmt extends RepeatedStmt instanceof LoopStmt { }
abstract class CheckEncodingCall extends MethodCall { }
@@ -111,9 +104,7 @@ private class RepeatedUrlDecodeCall extends MethodCall {
}
}
-private class CheckEncodingGuard extends Guard instanceof MethodCall {
- CheckEncodingGuard() { this instanceof CheckEncodingCall }
-
+private class CheckEncodingGuard extends Guard instanceof MethodCall, CheckEncodingCall {
Expr getCheckedExpr() { result = this.(MethodCall).getQualifier() }
}
From d9772c1880bb269c0c8e61db3beae8587bae8822 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 5 Mar 2024 15:04:43 -0500
Subject: [PATCH 21/40] Java: update change note
---
java/ql/src/change-notes/2023-12-12-url-forward-query.md | 4 ----
java/ql/src/change-notes/2024-03-06-url-forward-query.md | 4 ++++
2 files changed, 4 insertions(+), 4 deletions(-)
delete mode 100644 java/ql/src/change-notes/2023-12-12-url-forward-query.md
create mode 100644 java/ql/src/change-notes/2024-03-06-url-forward-query.md
diff --git a/java/ql/src/change-notes/2023-12-12-url-forward-query.md b/java/ql/src/change-notes/2023-12-12-url-forward-query.md
deleted file mode 100644
index 4efc4b7c4e0a..000000000000
--- a/java/ql/src/change-notes/2023-12-12-url-forward-query.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-category: newQuery
----
-* The query `java/unsafe-url-forward-dispatch-load` has been promoted from experimental to the main query pack. 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).
diff --git a/java/ql/src/change-notes/2024-03-06-url-forward-query.md b/java/ql/src/change-notes/2024-03-06-url-forward-query.md
new file mode 100644
index 000000000000..46028bda4f21
--- /dev/null
+++ b/java/ql/src/change-notes/2024-03-06-url-forward-query.md
@@ -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).
From d220b3a298044753382b0677d054e51da23a1246 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 10 Mar 2024 14:42:46 -0400
Subject: [PATCH 22/40] Java: some updates to test cases
---
.../code/java/security/PathSanitizer.qll | 9 +-
.../semmle/code/java/security/UrlForward.qll | 19 +-
.../security/CWE-552/UrlForwardTest.java | 163 +++++++++---------
3 files changed, 90 insertions(+), 101 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/PathSanitizer.qll b/java/ql/lib/semmle/code/java/security/PathSanitizer.qll
index f3c54629efde..77803e3e27dc 100644
--- a/java/ql/lib/semmle/code/java/security/PathSanitizer.qll
+++ b/java/ql/lib/semmle/code/java/security/PathSanitizer.qll
@@ -64,7 +64,10 @@ private predicate exactPathMatchGuard(Guard g, Expr e, boolean branch) {
)
}
-// TODO: switch back to private if possible
+/**
+ * A sanitizer that protects against path injection vulnerabilities
+ * by checking for a matching path.
+ */
class ExactPathMatchSanitizer extends PathInjectionSanitizer {
ExactPathMatchSanitizer() {
this = DataFlow::BarrierGuard::getABarrierNode()
@@ -152,8 +155,7 @@ private class DotDotCheckSanitizer extends PathInjectionSanitizer {
}
}
-// TODO: switch back to private if possible
-class BlockListGuard extends PathGuard instanceof MethodCall {
+private class BlockListGuard extends PathGuard instanceof MethodCall {
BlockListGuard() {
(isStringPartialMatch(this) or isPathPrefixMatch(this)) and
isDisallowedWord(super.getAnArgument())
@@ -230,7 +232,6 @@ private predicate isStringPartialMatch(MethodCall ma) {
exists(RefType t | t = ma.getMethod().getDeclaringType() |
t instanceof TypeString or t instanceof StringsKt
) and
- // TODO ! Why not use `StringPartialMatchMethod` for the below?
getSourceMethod(ma.getMethod())
.hasName(["contains", "matches", "regionMatches", "indexOf", "lastIndexOf"])
}
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index d19b8c163fdd..f7001023689b 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -50,23 +50,20 @@ private class FollowsBarrierPrefix extends UrlForwardBarrier {
private class BarrierPrefix extends InterestingPrefix {
BarrierPrefix() {
not this.getStringValue().matches("/WEB-INF/%") and
- not this.getStringValue() = "forward:"
+ not this instanceof ForwardPrefix
}
override int getOffset() { result = 0 }
}
-private class UrlPathBarrier extends UrlForwardBarrier {
+private class UrlPathBarrier extends UrlForwardBarrier instanceof PathInjectionSanitizer {
UrlPathBarrier() {
- this instanceof PathInjectionSanitizer and
- (
- this instanceof ExactPathMatchSanitizer //TODO: still need a better solution for this edge case...
- or
- // TODO: these don't enforce order of checks and PathSanitization... make bypass test cases.
- this instanceof NoEncodingBarrier
- or
- this instanceof FullyDecodesBarrier
- )
+ this instanceof ExactPathMatchSanitizer //TODO: still need a better solution for this edge case...
+ or
+ // TODO: these don't enforce order of checks and PathSanitization... make bypass test cases.
+ this instanceof NoEncodingBarrier
+ or
+ this instanceof FullyDecodesBarrier
}
}
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
index c7a9d82b1a0a..9b94f3f57248 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
@@ -23,7 +23,7 @@
@Controller
public class UrlForwardTest extends HttpServlet implements Filter {
- // (1) ORIGINAL
+ // Spring-related test cases
@GetMapping("/bad1")
public ModelAndView bad1(String url) {
return new ModelAndView(url); // $ hasUrlForward
@@ -91,7 +91,7 @@ public void good1(String url, HttpServletRequest request, HttpServletResponse re
}
}
- // (2) UnsafeRequestPath
+ // Non-Spring test cases (UnsafeRequest*Path*)
private static final String BASE_PATH = "/pages";
@Override
@@ -107,12 +107,12 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
}
}
- // GOOD: Request dispatcher from servlet path with check
+ // BAD: Request dispatcher from servlet path with check that does not decode
+ // the user-supplied path; could bypass check with ".." encoded as "%2e%2e".
public void doFilter2(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String path = ((HttpServletRequest) request).getServletPath();
- // actually BAD since could potentially bypass with ".." encoded as "%2e%2e"?
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
request.getRequestDispatcher(path).forward(request, response); // $ hasUrlForward
} else {
@@ -125,7 +125,6 @@ public void doFilter3(ServletRequest request, ServletResponse response, FilterCh
throws IOException, ServletException {
String path = ((HttpServletRequest) request).getServletPath();
- // this is still good, should not flag here..., url-decoding first doesn't matter if looking for exact match... :(
if (path.equals("/comaction")) {
request.getRequestDispatcher(path).forward(request, response);
} else {
@@ -133,7 +132,7 @@ public void doFilter3(ServletRequest request, ServletResponse response, FilterCh
}
}
- // (3) UnsafeServletRequestDispatch
+ // Non-Spring test cases (UnsafeServletRequest*Dispatch*)
@Override
// BAD: Request dispatcher constructed from `ServletContext` without input validation
protected void doGet(HttpServletRequest request, HttpServletResponse response)
@@ -190,41 +189,41 @@ protected void doHead2(HttpServletRequest request, HttpServletResponse response)
String path = request.getParameter("path");
// A sample payload "/pages/welcome.jsp/../WEB-INF/web.xml" can bypass the `startsWith` check
- // The payload "/pages/welcome.jsp/../../%57EB-INF/web.xml" can bypass the check as well since RequestDispatcher will decode `%57` as `W`
if (path.startsWith(BASE_PATH)) {
request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
}
}
- // GOOD: Request dispatcher with path traversal check
+ // BAD: Request dispatcher with path traversal check that does not decode
+ // the user-supplied path; could bypass check with ".." encoded as "%2e%2e".
protected void doHead3(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
- // actually BAD since could potentially bypass with ".." encoded as "%2e%2e"?
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
}
}
- // GOOD: Request dispatcher with path normalization and comparison
+ // BAD: Request dispatcher with path normalization and comparison, but
+ // does not decode before normalization.
protected void doHead4(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
+
+ // Since not decoded before normalization, "%2e%2e" can remain in the path
Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
- // /pages/welcome.jsp/../../WEB-INF/web.xml becomes /WEB-INF/web.xml
- // /pages/welcome.jsp/../../%57EB-INF/web.xml becomes /%57EB-INF/web.xml
- // actually BAD since could potentially bypass with ".." encoded as "%2e%2e": "/pages/welcome.jsp/%2e%2e/%2e%2e/WEB-INF/web.xml" becomes /pages/welcome.jsp/%2e%2e/%2e%2e/WEB-INF/web.xml, which will pass this check and potentially be problematic if decoded later?
if (requestedPath.startsWith(BASE_PATH)) {
request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ hasUrlForward
}
}
- // BAD (original FN): Request dispatcher with negation check and path normalization, but without URL decoding
+ // BAD: Request dispatcher with negation check and path normalization, but without URL decoding.
protected void doHead5(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
+ // Since not decoded before normalization, "/%57EB-INF" can remain in the path and pass the `startsWith` check.
Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
if (!requestedPath.startsWith("/WEB-INF") && !requestedPath.startsWith("/META-INF")) {
@@ -232,7 +231,7 @@ protected void doHead5(HttpServletRequest request, HttpServletResponse response)
}
}
- // BAD (I added to test decode with no loop): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
+ // BAD: Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
protected void doHead7(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
@@ -246,9 +245,9 @@ protected void doHead7(HttpServletRequest request, HttpServletResponse response)
// GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
protected void doHead6(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- String path = request.getParameter("path"); // v
+ String path = request.getParameter("path"); // TODO: remove this debugging comment: // v
- if (path.contains("%")){ // v.getAnAccess()
+ if (path.contains("%")){ // TODO: remove this debugging comment: // v.getAnAccess()
while (path.contains("%")) {
path = URLDecoder.decode(path, "UTF-8");
}
@@ -259,10 +258,53 @@ protected void doHead6(HttpServletRequest request, HttpServletResponse response)
}
}
+ // GOOD: Request dispatcher with URL encoding check and path traversal check
+ protected void doHead16(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+
+ if (!path.contains("%")){
+ if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ request.getServletContext().getRequestDispatcher(path).include(request, response);
+ }
+ }
+ }
+
+ // TODO: clean-up
+ // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
+ // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
+ // having previously been checked against a block-list of forbidden values."
+ protected void doHead10(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+ if (path.contains("%")){ // BAD: wrong check
+ if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ // if (path.contains("%")){ // BAD: wrong check
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
+ // }
+ }
+ }
+ }
+
+ // TODO: clean-up
+ // "GOOD" (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
+ // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
+ // having previously been checked against a block-list of forbidden values."
+ protected void doHead11(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getParameter("path");
+
+ if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
+ if (!path.contains("%")){ // GOOD: right check
+ request.getServletContext().getRequestDispatcher(path).include(request, response);
+ }
+ }
+ }
+
// GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
protected void doHead8(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- String path = request.getParameter("path"); // v
+ String path = request.getParameter("path"); // TODO: remove this debugging comment: // v
while (path.contains("%")) {
path = URLDecoder.decode(path, "UTF-8");
}
@@ -272,6 +314,7 @@ protected void doHead8(HttpServletRequest request, HttpServletResponse response)
}
}
+ // TODO: see if can fix?
// FP now....
// GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
protected void doHead9(HttpServletRequest request, HttpServletResponse response)
@@ -288,78 +331,26 @@ protected void doHead9(HttpServletRequest request, HttpServletResponse response)
}
}
- // New Tests
+ // BAD: `StaplerResponse.forward` without any checks
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object obj) throws IOException, ServletException {
String url = req.getParameter("target");
rsp.forward(obj, url, req); // $ hasUrlForward
}
- // Other Tests for edge cases:
- // // GOOD (I added): Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
- // // testing `if` stmt requirement for BB controlling
- // protected void doHead12(HttpServletRequest request, HttpServletResponse response)
- // throws ServletException, IOException {
- // String path = request.getParameter("path");
- // if (path.contains("%")) {
- // while (path.contains("%")) {
- // path = URLDecoder.decode(path, "UTF-8");
- // }
- // }
- // if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- // request.getServletContext().getRequestDispatcher(path).include(request, response);
- // }
- // }
- // // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
- // // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
- // // having previously been checked against a block-list of forbidden values."
- // protected void doHead8(HttpServletRequest request, HttpServletResponse response)
- // throws ServletException, IOException {
- // String path = request.getParameter("path");
-
- // if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- // boolean hasEncoding = path.contains("%"); // BAD: doesn't do anything with the check...
- // request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
- // }
- // }
- // // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
- // // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
- // // having previously been checked against a block-list of forbidden values."
- // protected void doHead9(HttpServletRequest request, HttpServletResponse response)
- // throws ServletException, IOException {
- // String path = request.getParameter("path");
-
- // boolean hasEncoding = path.contains("%"); // BAD: doesn't do anything with the check... and check comes BEFORE blocklist so guard should not trigger
- // if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- // request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
- // }
- // }
-
- // // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
- // // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
- // // having previously been checked against a block-list of forbidden values."
- // protected void doHead10(HttpServletRequest request, HttpServletResponse response)
- // throws ServletException, IOException {
- // String path = request.getParameter("path");
-
- // if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- // if (path.contains("%")){ // BAD: wrong check
- // request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
- // }
- // }
- // }
-
- // // "GOOD" (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
- // // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
- // // having previously been checked against a block-list of forbidden values."
- // protected void doHead11(HttpServletRequest request, HttpServletResponse response)
- // throws ServletException, IOException {
- // String path = request.getParameter("path");
-
- // if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- // if (!path.contains("%")){ // GOOD: right check
- // request.getServletContext().getRequestDispatcher(path).include(request, response);
- // }
- // }
- // }
+ // QHelp example
+ private static final String VALID_FORWARD = "https://cwe.mitre.org/data/definitions/552.html";
+
+ protected void doGet2(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); // $ hasUrlForward
+
+ // 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);
+ }
+ }
}
From 052452b18666e793783032da33fd1863e5d7121f Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 10 Mar 2024 15:54:11 -0400
Subject: [PATCH 23/40] Java: create UrlDecodeMethod
---
.../lib/semmle/code/java/frameworks/Networking.qll | 13 +++++++++++++
.../ql/lib/semmle/code/java/security/UrlForward.qll | 7 ++-----
2 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/frameworks/Networking.qll b/java/ql/lib/semmle/code/java/frameworks/Networking.qll
index c473cc9fc09c..f86cecd5b4ee 100644
--- a/java/ql/lib/semmle/code/java/frameworks/Networking.qll
+++ b/java/ql/lib/semmle/code/java/frameworks/Networking.qll
@@ -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") }
@@ -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() {
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index f7001023689b..be9bfb91043e 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -41,8 +41,6 @@ abstract class UrlForwardBarrier extends DataFlow::Node { }
private class PrimitiveBarrier extends UrlForwardBarrier instanceof SimpleTypeSanitizer { }
-// TODO: should this also take URL encoding/decoding into account?
-// TODO: and PathSanitization in general?
private class FollowsBarrierPrefix extends UrlForwardBarrier {
FollowsBarrierPrefix() { this.asExpr() = any(BarrierPrefix fp).getAnAppendedExpression() }
}
@@ -58,9 +56,8 @@ private class BarrierPrefix extends InterestingPrefix {
private class UrlPathBarrier extends UrlForwardBarrier instanceof PathInjectionSanitizer {
UrlPathBarrier() {
- this instanceof ExactPathMatchSanitizer //TODO: still need a better solution for this edge case...
+ this instanceof ExactPathMatchSanitizer
or
- // TODO: these don't enforce order of checks and PathSanitization... make bypass test cases.
this instanceof NoEncodingBarrier
or
this instanceof FullyDecodesBarrier
@@ -71,7 +68,7 @@ abstract class UrlDecodeCall extends MethodCall { }
private class DefaultUrlDecodeCall extends UrlDecodeCall {
DefaultUrlDecodeCall() {
- this.getMethod().hasQualifiedName("java.net", "URLDecoder", "decode") or // TODO: reuse existing class? Or make this a class?
+ this.getMethod() instanceof UrlDecodeMethod or
this.getMethod().hasQualifiedName("org.eclipse.jetty.util.URIUtil", "URIUtil", "decodePath")
}
}
From 042dcf9cd961e2b24b30220b8f7618221022f339 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 10 Mar 2024 16:50:41 -0400
Subject: [PATCH 24/40] Java: some updates to UrlPathBarrier code
---
java/ql/lib/semmle/code/java/JDK.qll | 7 ++
.../semmle/code/java/security/UrlForward.qll | 69 +++++++++----------
2 files changed, 38 insertions(+), 38 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/JDK.qll b/java/ql/lib/semmle/code/java/JDK.qll
index 7623cc87393c..55d420dbcaec 100644
--- a/java/ql/lib/semmle/code/java/JDK.qll
+++ b/java/ql/lib/semmle/code/java/JDK.qll
@@ -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.
*/
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index be9bfb91043e..8f1b978cbfcc 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -58,12 +58,13 @@ private class UrlPathBarrier extends UrlForwardBarrier instanceof PathInjectionS
UrlPathBarrier() {
this instanceof ExactPathMatchSanitizer
or
- this instanceof NoEncodingBarrier
+ this instanceof NoUrlEncodingBarrier
or
- this instanceof FullyDecodesBarrier
+ this instanceof FullyDecodesUrlBarrier
}
}
+/** A call to a method that decodes a URL. */
abstract class UrlDecodeCall extends MethodCall { }
private class DefaultUrlDecodeCall extends UrlDecodeCall {
@@ -73,77 +74,69 @@ private class DefaultUrlDecodeCall extends UrlDecodeCall {
}
}
-// TODO: this can probably be named/designed better...
-abstract class RepeatedStmt extends Stmt { }
+/** A repeated call to a method that decodes a URL. */
+abstract class RepeatedUrlDecodeCall extends MethodCall { }
-private class DefaultRepeatedStmt extends RepeatedStmt instanceof LoopStmt { }
-
-abstract class CheckEncodingCall extends MethodCall { }
-
-private class DefaultCheckEncodingCall extends CheckEncodingCall {
- DefaultCheckEncodingCall() {
- // TODO: indexOf?, etc.
- this.getMethod().hasQualifiedName("java.lang", "String", "contains") and // TODO: reuse existing class? Or make this a class?
- this.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "%"
+private class DefaultRepeatedUrlDecodeCall extends RepeatedUrlDecodeCall {
+ DefaultRepeatedUrlDecodeCall() {
+ this instanceof UrlDecodeCall and
+ this.getAnEnclosingStmt() instanceof LoopStmt
}
}
-// TODO: better naming?
-// TODO: check if any URL decoding implementations _fully_ decode... or if all need to be called in a loop?
-// TODO: make this extendable instead of `RepeatedStmt`?
-private class RepeatedUrlDecodeCall extends MethodCall {
- RepeatedUrlDecodeCall() {
- this instanceof UrlDecodeCall and
- this.getAnEnclosingStmt() instanceof RepeatedStmt
+/** 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() = "%"
}
}
-private class CheckEncodingGuard extends Guard instanceof MethodCall, CheckEncodingCall {
+private class CheckUrlEncodingGuard extends Guard instanceof CheckUrlEncodingCall {
Expr getCheckedExpr() { result = this.(MethodCall).getQualifier() }
}
-private predicate noEncodingGuard(Guard g, Expr e, boolean branch) {
- g instanceof CheckEncodingGuard and
- e = g.(CheckEncodingGuard).getCheckedExpr() and
+private predicate noUrlEncodingGuard(Guard g, Expr e, boolean branch) {
+ g instanceof CheckUrlEncodingGuard and
+ e = g.(CheckUrlEncodingGuard).getCheckedExpr() and
branch = false
or
- // branch = false and
- // g instanceof AssignExpr and // AssignExpr
- // exists(CheckEncodingCall call | g.(AssignExpr).getSource() = call | e = call.getQualifier())
branch = false and
- g.(Expr).getType() instanceof BooleanType and // AssignExpr
+ g.(Expr).getType() instanceof BooleanType and // TODO: remove debugging comment: // AssignExpr
(
- exists(CheckEncodingCall call, AssignExpr ae |
+ exists(CheckUrlEncodingCall call, AssignExpr ae |
ae.getSource() = call and
e = call.getQualifier() and
g = ae.getDest()
)
or
- exists(CheckEncodingCall call, LocalVariableDeclExpr vde |
+ exists(CheckUrlEncodingCall call, LocalVariableDeclExpr vde |
vde.getInitOrPatternSource() = call and
e = call.getQualifier() and
g = vde.getAnAccess() //and
//vde.getVariable() = g
+ // TODO: remove debugging comments above
)
)
}
-// TODO: check edge case of !contains(%), make sure that behaves as expected at least.
-private class NoEncodingBarrier extends DataFlow::Node {
- NoEncodingBarrier() { this = DataFlow::BarrierGuard::getABarrierNode() }
+private class NoUrlEncodingBarrier extends DataFlow::Node {
+ NoUrlEncodingBarrier() { this = DataFlow::BarrierGuard::getABarrierNode() }
}
-private predicate fullyDecodesGuard(Expr e) {
- exists(CheckEncodingGuard g, RepeatedUrlDecodeCall decodeCall |
+private predicate fullyDecodesUrlGuard(Expr e) {
+ exists(CheckUrlEncodingGuard g, RepeatedUrlDecodeCall decodeCall |
e = g.getCheckedExpr() and
g.controls(decodeCall.getBasicBlock(), true)
)
}
-private class FullyDecodesBarrier extends DataFlow::Node {
- FullyDecodesBarrier() {
+private class FullyDecodesUrlBarrier extends DataFlow::Node {
+ FullyDecodesUrlBarrier() {
exists(Variable v, Expr e | this.asExpr() = v.getAnAccess() |
- fullyDecodesGuard(e) and
+ fullyDecodesUrlGuard(e) and
e = v.getAnAccess() and
e.getBasicBlock().bbDominates(this.asExpr().getBasicBlock())
)
From a8075969d886bc86ef60b12f10bdf551f4145eba Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 10 Mar 2024 18:33:00 -0400
Subject: [PATCH 25/40] Java: add QLDocs to UrlPathBarrier code
---
.../semmle/code/java/security/UrlForward.qll | 21 +++++++++++--------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index 8f1b978cbfcc..d4cad4e2f54d 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -41,10 +41,12 @@ abstract class UrlForwardBarrier extends DataFlow::Node { }
private class PrimitiveBarrier extends UrlForwardBarrier instanceof SimpleTypeSanitizer { }
+// TODO: QLDoc
private class FollowsBarrierPrefix extends UrlForwardBarrier {
FollowsBarrierPrefix() { this.asExpr() = any(BarrierPrefix fp).getAnAppendedExpression() }
}
+// TODO: QLDoc and fix broadness of this prefix check...
private class BarrierPrefix extends InterestingPrefix {
BarrierPrefix() {
not this.getStringValue().matches("/WEB-INF/%") and
@@ -54,6 +56,7 @@ private class BarrierPrefix extends InterestingPrefix {
override int getOffset() { result = 0 }
}
+/** A barrier that protects against path injection vulnerabilities while accounting for URL encoding. */
private class UrlPathBarrier extends UrlForwardBarrier instanceof PathInjectionSanitizer {
UrlPathBarrier() {
this instanceof ExactPathMatchSanitizer
@@ -77,11 +80,8 @@ private class DefaultUrlDecodeCall extends UrlDecodeCall {
/** A repeated call to a method that decodes a URL. */
abstract class RepeatedUrlDecodeCall extends MethodCall { }
-private class DefaultRepeatedUrlDecodeCall extends RepeatedUrlDecodeCall {
- DefaultRepeatedUrlDecodeCall() {
- this instanceof UrlDecodeCall and
- this.getAnEnclosingStmt() instanceof LoopStmt
- }
+private class DefaultRepeatedUrlDecodeCall extends RepeatedUrlDecodeCall instanceof UrlDecodeCall {
+ DefaultRepeatedUrlDecodeCall() { this.getAnEnclosingStmt() instanceof LoopStmt }
}
/** A method call that checks a string for URL encoding. */
@@ -94,17 +94,19 @@ private class DefaultCheckUrlEncodingCall extends CheckUrlEncodingCall {
}
}
+/** 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) {
g instanceof CheckUrlEncodingGuard and
e = g.(CheckUrlEncodingGuard).getCheckedExpr() and
branch = false
or
branch = false and
- g.(Expr).getType() instanceof BooleanType and // TODO: remove debugging comment: // AssignExpr
+ g.(Expr).getType() instanceof BooleanType and
(
exists(CheckUrlEncodingCall call, AssignExpr ae |
ae.getSource() = call and
@@ -115,17 +117,17 @@ private predicate noUrlEncodingGuard(Guard g, Expr e, boolean branch) {
exists(CheckUrlEncodingCall call, LocalVariableDeclExpr vde |
vde.getInitOrPatternSource() = call and
e = call.getQualifier() and
- g = vde.getAnAccess() //and
- //vde.getVariable() = g
- // TODO: remove debugging comments above
+ g = vde.getAnAccess()
)
)
}
+/** A barrier for URLs that do not contain URL encoding. */
private class NoUrlEncodingBarrier extends DataFlow::Node {
NoUrlEncodingBarrier() { this = DataFlow::BarrierGuard::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
@@ -133,6 +135,7 @@ private predicate fullyDecodesUrlGuard(Expr e) {
)
}
+/** A barrier for URLs that are fully decoded. */
private class FullyDecodesUrlBarrier extends DataFlow::Node {
FullyDecodesUrlBarrier() {
exists(Variable v, Expr e | this.asExpr() = v.getAnAccess() |
From a002674587b255694088bed6768b8c16cf6ffac9 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 10 Mar 2024 21:22:35 -0400
Subject: [PATCH 26/40] Java: clean up comments on test cases
---
.../security/CWE-552/UrlForwardTest.java | 59 ++++++++-----------
1 file changed, 26 insertions(+), 33 deletions(-)
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
index 9b94f3f57248..e66b5c899c72 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
@@ -23,7 +23,7 @@
@Controller
public class UrlForwardTest extends HttpServlet implements Filter {
- // Spring-related test cases
+ // Spring `ModelAndView` test cases
@GetMapping("/bad1")
public ModelAndView bad1(String url) {
return new ModelAndView(url); // $ hasUrlForward
@@ -36,6 +36,7 @@ public ModelAndView bad2(String url) {
return modelAndView;
}
+ // Spring `"forward:"` prefix test cases
@GetMapping("/bad3")
public String bad3(String url) {
return "forward:" + url + "/swagger-ui/index.html"; // $ hasUrlForward
@@ -47,6 +48,7 @@ public ModelAndView bad4(String url) {
return modelAndView;
}
+ // `RequestDispatcher` test cases from a Spring `GetMapping` entry point
@GetMapping("/bad5")
public void bad5(String url, HttpServletRequest request, HttpServletResponse response) {
try {
@@ -91,7 +93,7 @@ public void good1(String url, HttpServletRequest request, HttpServletResponse re
}
}
- // Non-Spring test cases (UnsafeRequest*Path*)
+ // `RequestDispatcher` test cases from non-Spring entry points
private static final String BASE_PATH = "/pages";
@Override
@@ -132,7 +134,6 @@ public void doFilter3(ServletRequest request, ServletResponse response, FilterCh
}
}
- // Non-Spring test cases (UnsafeServletRequest*Dispatch*)
@Override
// BAD: Request dispatcher constructed from `ServletContext` without input validation
protected void doGet(HttpServletRequest request, HttpServletResponse response)
@@ -184,7 +185,7 @@ protected void doPut(HttpServletRequest request, HttpServletResponse response)
}
// BAD: Request dispatcher without path traversal check
- protected void doHead2(HttpServletRequest request, HttpServletResponse response)
+ protected void doHead1(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
@@ -196,7 +197,7 @@ protected void doHead2(HttpServletRequest request, HttpServletResponse response)
// BAD: Request dispatcher with path traversal check that does not decode
// the user-supplied path; could bypass check with ".." encoded as "%2e%2e".
- protected void doHead3(HttpServletRequest request, HttpServletResponse response)
+ protected void doHead2(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
@@ -207,7 +208,7 @@ protected void doHead3(HttpServletRequest request, HttpServletResponse response)
// BAD: Request dispatcher with path normalization and comparison, but
// does not decode before normalization.
- protected void doHead4(HttpServletRequest request, HttpServletResponse response)
+ protected void doHead3(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
@@ -220,7 +221,7 @@ protected void doHead4(HttpServletRequest request, HttpServletResponse response)
}
// BAD: Request dispatcher with negation check and path normalization, but without URL decoding.
- protected void doHead5(HttpServletRequest request, HttpServletResponse response)
+ protected void doHead4(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
// Since not decoded before normalization, "/%57EB-INF" can remain in the path and pass the `startsWith` check.
@@ -232,7 +233,7 @@ protected void doHead5(HttpServletRequest request, HttpServletResponse response)
}
// BAD: Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
- protected void doHead7(HttpServletRequest request, HttpServletResponse response)
+ protected void doHead5(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
path = URLDecoder.decode(path, "UTF-8");
@@ -245,9 +246,9 @@ protected void doHead7(HttpServletRequest request, HttpServletResponse response)
// GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
protected void doHead6(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- String path = request.getParameter("path"); // TODO: remove this debugging comment: // v
+ String path = request.getParameter("path");
- if (path.contains("%")){ // TODO: remove this debugging comment: // v.getAnAccess()
+ if (path.contains("%")){
while (path.contains("%")) {
path = URLDecoder.decode(path, "UTF-8");
}
@@ -259,7 +260,7 @@ protected void doHead6(HttpServletRequest request, HttpServletResponse response)
}
// GOOD: Request dispatcher with URL encoding check and path traversal check
- protected void doHead16(HttpServletRequest request, HttpServletResponse response)
+ protected void doHead7(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
@@ -270,41 +271,33 @@ protected void doHead16(HttpServletRequest request, HttpServletResponse response
}
}
- // TODO: clean-up
- // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
- // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
- // having previously been checked against a block-list of forbidden values."
- protected void doHead10(HttpServletRequest request, HttpServletResponse response)
+ // BAD: Request dispatcher without URL decoding before WEB-INF and path traversal checks
+ protected void doHead8(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
- if (path.contains("%")){ // BAD: wrong check
- if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- // if (path.contains("%")){ // BAD: wrong check
+ if (path.contains("%")){ // incorrect check
+ if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
- // }
+ }
}
}
- }
- // TODO: clean-up
- // "GOOD" (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
- // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
- // having previously been checked against a block-list of forbidden values."
- protected void doHead11(HttpServletRequest request, HttpServletResponse response)
+ // GOOD: Request dispatcher with WEB-INF, path traversal, and URL encoding checks
+ protected void doHead9(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");
if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- if (!path.contains("%")){ // GOOD: right check
+ if (!path.contains("%")){ // correct check
request.getServletContext().getRequestDispatcher(path).include(request, response);
}
}
}
// GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
- protected void doHead8(HttpServletRequest request, HttpServletResponse response)
+ protected void doHead10(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- String path = request.getParameter("path"); // TODO: remove this debugging comment: // v
+ String path = request.getParameter("path");
while (path.contains("%")) {
path = URLDecoder.decode(path, "UTF-8");
}
@@ -314,12 +307,12 @@ protected void doHead8(HttpServletRequest request, HttpServletResponse response)
}
}
- // TODO: see if can fix?
- // FP now....
// GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
- protected void doHead9(HttpServletRequest request, HttpServletResponse response)
+ protected void doHead11(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- String path = request.getParameter("path"); // v
+ String path = request.getParameter("path");
+ // FP: we don't currently handle the scenario where the
+ // `path.contains("%")` check is stored in a variable.
boolean hasEncoding = path.contains("%");
while (hasEncoding) {
path = URLDecoder.decode(path, "UTF-8");
From 7310c155e24a238316224adf9cff9204f174d843 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 10 Mar 2024 21:29:00 -0400
Subject: [PATCH 27/40] Java: rename SpringUrlForwardSink
---
java/ql/lib/semmle/code/java/security/UrlForward.qll | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index d4cad4e2f54d..76ef139b7b2f 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -23,8 +23,8 @@ private class DefaultUrlForwardSink extends UrlForwardSink {
* An expression appended (perhaps indirectly) to `"forward:"`
* and reachable from a Spring entry point.
*/
-private class SpringUrlForwardSink extends UrlForwardSink {
- SpringUrlForwardSink() {
+private class SpringUrlForwardPrefixSink extends UrlForwardSink {
+ SpringUrlForwardPrefixSink() {
any(SpringRequestMappingMethod srmm).polyCalls*(this.getEnclosingCallable()) and
this.asExpr() = any(ForwardPrefix fp).getAnAppendedExpression()
}
From c5a59d6c514cbe2c8985ada441bdd5203b9c615e Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 10 Mar 2024 23:27:22 -0400
Subject: [PATCH 28/40] Java: add QLDoc
---
java/ql/lib/semmle/code/java/security/UrlForward.qll | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index 76ef139b7b2f..e72f3ab21173 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -41,12 +41,11 @@ abstract class UrlForwardBarrier extends DataFlow::Node { }
private class PrimitiveBarrier extends UrlForwardBarrier instanceof SimpleTypeSanitizer { }
-// TODO: QLDoc
+/** A barrier for URLs appended to a prefix. */
private class FollowsBarrierPrefix extends UrlForwardBarrier {
FollowsBarrierPrefix() { this.asExpr() = any(BarrierPrefix fp).getAnAppendedExpression() }
}
-// TODO: QLDoc and fix broadness of this prefix check...
private class BarrierPrefix extends InterestingPrefix {
BarrierPrefix() {
not this.getStringValue().matches("/WEB-INF/%") and
From e99cea340bcf8902bf133c8913cf867d943475c9 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 12 Mar 2024 12:21:22 -0400
Subject: [PATCH 29/40] Java: update UrlPathBarrier to include
FollowsBarrierPrefix
---
.../semmle/code/java/security/UrlForward.qll | 17 ++++-----
.../security/CWE-552/UrlForwardTest.java | 36 ++++++++++++++++++-
2 files changed, 44 insertions(+), 9 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index e72f3ab21173..8c7f8d55eb0f 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -41,8 +41,7 @@ abstract class UrlForwardBarrier extends DataFlow::Node { }
private class PrimitiveBarrier extends UrlForwardBarrier instanceof SimpleTypeSanitizer { }
-/** A barrier for URLs appended to a prefix. */
-private class FollowsBarrierPrefix extends UrlForwardBarrier {
+private class FollowsBarrierPrefix extends DataFlow::Node {
FollowsBarrierPrefix() { this.asExpr() = any(BarrierPrefix fp).getAnAppendedExpression() }
}
@@ -55,14 +54,16 @@ private class BarrierPrefix extends InterestingPrefix {
override int getOffset() { result = 0 }
}
-/** A barrier that protects against path injection vulnerabilities while accounting for URL encoding. */
+/**
+ * A barrier that protects against path injection vulnerabilities while accounting
+ * for URL encoding and concatenated prefixes.
+ */
private class UrlPathBarrier extends UrlForwardBarrier instanceof PathInjectionSanitizer {
UrlPathBarrier() {
- this instanceof ExactPathMatchSanitizer
- or
- this instanceof NoUrlEncodingBarrier
- or
- this instanceof FullyDecodesUrlBarrier
+ this instanceof ExactPathMatchSanitizer or
+ this instanceof NoUrlEncodingBarrier or
+ this instanceof FullyDecodesUrlBarrier or
+ this instanceof FollowsBarrierPrefix
}
}
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
index e66b5c899c72..6d1c0580cb68 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
@@ -85,7 +85,41 @@ public void bad7(String url, HttpServletRequest request, HttpServletResponse res
@GetMapping("/good1")
public void good1(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher("/index.jsp?token=" + url).forward(request, response);
+ request.getRequestDispatcher("/index.jsp?token=" + url).forward(request, response); // $ SPURIOUS: hasUrlForward
+ } catch (ServletException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // BAD: appended to a prefix without path sanitization
+ @GetMapping("/bad8")
+ public void bad8(String urlPath, HttpServletRequest request, HttpServletResponse response) {
+ try {
+ String url = "/pages" + urlPath;
+ request.getRequestDispatcher(url).forward(request, response); // $ hasUrlForward
+ } catch (ServletException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // GOOD: appended to a prefix with path sanitization
+ @GetMapping("/good2")
+ public void good2(String urlPath, HttpServletRequest request, HttpServletResponse response) {
+ try {
+ while (urlPath.contains("%")) {
+ urlPath = URLDecoder.decode(urlPath, "UTF-8");
+ }
+
+ if (!urlPath.contains("..") && !urlPath.startsWith("/WEB-INF")) {
+ // Note: path injection sanitizer does not account for string concatenation instead of a `startswith` check
+ String url = "/pages" + urlPath;
+ request.getRequestDispatcher(url).forward(request, response);
+ }
+
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
From 04d27f2d65e9fa1833731889404e57c33f9a22f0 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 12 Mar 2024 20:44:53 -0400
Subject: [PATCH 30/40] Java: adjust prefix barriers
---
.../semmle/code/java/security/UrlForward.qll | 37 +++++++++++++++----
.../security/CWE-552/UrlForwardTest.java | 10 ++++-
2 files changed, 38 insertions(+), 9 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index 8c7f8d55eb0f..5ea36d7c6b8b 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -41,29 +41,50 @@ abstract class UrlForwardBarrier extends DataFlow::Node { }
private class PrimitiveBarrier extends UrlForwardBarrier instanceof SimpleTypeSanitizer { }
-private class FollowsBarrierPrefix extends DataFlow::Node {
+/**
+ * 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() {
- not this.getStringValue().matches("/WEB-INF/%") and
- not this instanceof ForwardPrefix
+ // 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))
}
- override int getOffset() { result = 0 }
+ override int getOffset() { result = offset }
}
/**
- * A barrier that protects against path injection vulnerabilities while accounting
- * for URL encoding and concatenated prefixes.
+ * 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 or
- this instanceof FollowsBarrierPrefix
+ this instanceof FullyDecodesUrlBarrier
}
}
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
index 6d1c0580cb68..19bd739c294a 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
@@ -48,6 +48,14 @@ public ModelAndView bad4(String url) {
return modelAndView;
}
+ // Not relevant for this query since redirecting instead of forwarding
+ // This result should be found by the `java/unvalidated-url-redirection` query instead.
+ @GetMapping("/redirect")
+ public ModelAndView redirect(String url) {
+ ModelAndView modelAndView = new ModelAndView("redirect:" + url);
+ return modelAndView;
+ }
+
// `RequestDispatcher` test cases from a Spring `GetMapping` entry point
@GetMapping("/bad5")
public void bad5(String url, HttpServletRequest request, HttpServletResponse response) {
@@ -85,7 +93,7 @@ public void bad7(String url, HttpServletRequest request, HttpServletResponse res
@GetMapping("/good1")
public void good1(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher("/index.jsp?token=" + url).forward(request, response); // $ SPURIOUS: hasUrlForward
+ request.getRequestDispatcher("/index.jsp?token=" + url).forward(request, response);
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
From 5ac453eb38813b14f0e62c2215486581c3943b87 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 13 Mar 2024 09:36:42 -0400
Subject: [PATCH 31/40] Java: add spurious test case for StringBuilder.append
---
.../security/CWE-552/UrlForwardTest.java | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
index 19bd739c294a..5d1d19d4be51 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
@@ -388,4 +388,25 @@ protected void doGet2(HttpServletRequest request, HttpServletResponse response)
sc.getRequestDispatcher(VALID_FORWARD).forward(request, response);
}
}
+
+ // Test `StringBuilder.append` sequence with `?` appended before the user input
+ private static final String LOGIN_URL = "/UI/Login";
+
+ public void doPost2(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ StringBuilder forwardUrl = new StringBuilder(200);
+ forwardUrl.append(LOGIN_URL);
+
+ String queryString = request.getQueryString();
+
+ // should be sanitized due to the `?` appended
+ forwardUrl.append('?').append(queryString);
+
+ String fUrl = forwardUrl.toString();
+
+ ServletConfig config = getServletConfig();
+
+ RequestDispatcher dispatcher = config.getServletContext().getRequestDispatcher(fUrl); // $ SPURIOUS: hasUrlForward
+ dispatcher.forward(request, response);
+ }
}
From 1b01f26d09dc118c3796846ceaa975525c3ccf6f Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 13 Mar 2024 16:27:25 -0400
Subject: [PATCH 32/40] Java: adjust BarrierPrefix to handle prepended chars
---
java/ql/lib/semmle/code/java/security/UrlForward.qll | 2 ++
.../ql/test/query-tests/security/CWE-552/UrlForwardTest.java | 5 ++---
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index 5ea36d7c6b8b..b8cc6821abff 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -71,6 +71,8 @@ private class BarrierPrefix extends InterestingPrefix {
// 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 }
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
index 5d1d19d4be51..f0e982c74003 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
@@ -389,7 +389,7 @@ protected void doGet2(HttpServletRequest request, HttpServletResponse response)
}
}
- // Test `StringBuilder.append` sequence with `?` appended before the user input
+ // GOOD: char `?` appended before the user input
private static final String LOGIN_URL = "/UI/Login";
public void doPost2(HttpServletRequest request, HttpServletResponse response)
@@ -399,14 +399,13 @@ public void doPost2(HttpServletRequest request, HttpServletResponse response)
String queryString = request.getQueryString();
- // should be sanitized due to the `?` appended
forwardUrl.append('?').append(queryString);
String fUrl = forwardUrl.toString();
ServletConfig config = getServletConfig();
- RequestDispatcher dispatcher = config.getServletContext().getRequestDispatcher(fUrl); // $ SPURIOUS: hasUrlForward
+ RequestDispatcher dispatcher = config.getServletContext().getRequestDispatcher(fUrl);
dispatcher.forward(request, response);
}
}
From 55f7369df0d44893dd4cd4d843b7e740cc764854 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Fri, 15 Mar 2024 14:06:36 -0400
Subject: [PATCH 33/40] Java: performance fix
---
java/ql/lib/semmle/code/java/security/UrlForward.qll | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index b8cc6821abff..464a125ef758 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -26,10 +26,15 @@ private class DefaultUrlForwardSink extends UrlForwardSink {
private class SpringUrlForwardPrefixSink extends UrlForwardSink {
SpringUrlForwardPrefixSink() {
any(SpringRequestMappingMethod srmm).polyCalls*(this.getEnclosingCallable()) and
- this.asExpr() = any(ForwardPrefix fp).getAnAppendedExpression()
+ 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:" }
From 658fffeac1acaf6e3b823635efd27df7f611584f Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 17 Mar 2024 22:03:59 -0400
Subject: [PATCH 34/40] Java: remove experimental files
---
...ndertow.server.handlers.resource.model.yml | 8 -
.../jakarta.servlet.http.model.yml | 6 -
.../ext/experimental/java.nio.file.model.yml | 10 --
.../java.util.concurrent.model.yml | 1 -
.../experimental/javax.servlet.http.model.yml | 6 -
.../org.springframework.core.io.model.yml | 16 --
.../CWE/CWE-552/UnsafeLoadSpringResource.java | 21 ---
.../CWE/CWE-552/UnsafeResourceGet.java | 18 --
.../CWE-552/UnsafeServletRequestDispatch.java | 11 --
.../CWE/CWE-552/UnsafeUrlForward.java | 38 ----
.../CWE/CWE-552/UnsafeUrlForward.qhelp | 70 --------
.../Security/CWE/CWE-552/UnsafeUrlForward.ql | 64 -------
.../Security/CWE/CWE-552/UnsafeUrlForward.qll | 163 ------------------
13 files changed, 432 deletions(-)
delete mode 100644 java/ql/lib/ext/experimental/io.undertow.server.handlers.resource.model.yml
delete mode 100644 java/ql/lib/ext/experimental/jakarta.servlet.http.model.yml
delete mode 100644 java/ql/lib/ext/experimental/java.nio.file.model.yml
delete mode 100644 java/ql/lib/ext/experimental/org.springframework.core.io.model.yml
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-552/UnsafeLoadSpringResource.java
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-552/UnsafeResourceGet.java
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.java
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qhelp
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.ql
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qll
diff --git a/java/ql/lib/ext/experimental/io.undertow.server.handlers.resource.model.yml b/java/ql/lib/ext/experimental/io.undertow.server.handlers.resource.model.yml
deleted file mode 100644
index 5c86c75522c9..000000000000
--- a/java/ql/lib/ext/experimental/io.undertow.server.handlers.resource.model.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-extensions:
- - addsTo:
- pack: codeql/java-all
- extensible: experimentalSummaryModel
- data:
- - ["io.undertow.server.handlers.resource", "Resource", True, "getFile", "", "", "Argument[this]", "ReturnValue", "taint", "manual", "unsafe-url-forward"]
- - ["io.undertow.server.handlers.resource", "Resource", True, "getFilePath", "", "", "Argument[this]", "ReturnValue", "taint", "manual", "unsafe-url-forward"]
- - ["io.undertow.server.handlers.resource", "Resource", True, "getPath", "", "", "Argument[this]", "ReturnValue", "taint", "manual", "unsafe-url-forward"]
diff --git a/java/ql/lib/ext/experimental/jakarta.servlet.http.model.yml b/java/ql/lib/ext/experimental/jakarta.servlet.http.model.yml
deleted file mode 100644
index 9500beba15b6..000000000000
--- a/java/ql/lib/ext/experimental/jakarta.servlet.http.model.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-extensions:
- - addsTo:
- pack: codeql/java-all
- extensible: experimentalSourceModel
- data:
- - ["jakarta.servlet.http", "HttpServletRequest", True, "getServletPath", "", "", "ReturnValue", "remote", "manual", "unsafe-url-forward"]
diff --git a/java/ql/lib/ext/experimental/java.nio.file.model.yml b/java/ql/lib/ext/experimental/java.nio.file.model.yml
deleted file mode 100644
index 647d72329d0b..000000000000
--- a/java/ql/lib/ext/experimental/java.nio.file.model.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-extensions:
- - addsTo:
- pack: codeql/java-all
- extensible: experimentalSummaryModel
- data:
- - ["java.nio.file", "Path", True, "normalize", "", "", "Argument[this]", "ReturnValue", "taint", "manual", "unsafe-url-forward"]
- - ["java.nio.file", "Path", True, "resolve", "", "", "Argument[this]", "ReturnValue", "taint", "manual", "unsafe-url-forward"]
- - ["java.nio.file", "Path", True, "resolve", "", "", "Argument[0]", "ReturnValue", "taint", "manual", "unsafe-url-forward"]
- - ["java.nio.file", "Path", True, "toString", "", "", "Argument[this]", "ReturnValue", "taint", "manual", "unsafe-url-forward"]
- - ["java.nio.file", "Paths", True, "get", "", "", "Argument[0..1]", "ReturnValue", "taint", "manual", "unsafe-url-forward"]
diff --git a/java/ql/lib/ext/experimental/java.util.concurrent.model.yml b/java/ql/lib/ext/experimental/java.util.concurrent.model.yml
index 82ff0a00570a..9484a5f5eb96 100644
--- a/java/ql/lib/ext/experimental/java.util.concurrent.model.yml
+++ b/java/ql/lib/ext/experimental/java.util.concurrent.model.yml
@@ -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"]
diff --git a/java/ql/lib/ext/experimental/javax.servlet.http.model.yml b/java/ql/lib/ext/experimental/javax.servlet.http.model.yml
index db140149a99f..04681b300cab 100644
--- a/java/ql/lib/ext/experimental/javax.servlet.http.model.yml
+++ b/java/ql/lib/ext/experimental/javax.servlet.http.model.yml
@@ -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
@@ -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"]
-
diff --git a/java/ql/lib/ext/experimental/org.springframework.core.io.model.yml b/java/ql/lib/ext/experimental/org.springframework.core.io.model.yml
deleted file mode 100644
index e929260f21bf..000000000000
--- a/java/ql/lib/ext/experimental/org.springframework.core.io.model.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-extensions:
- - addsTo:
- pack: codeql/java-all
- extensible: experimentalSinkModel
- data:
- - ["org.springframework.core.io", "ClassPathResource", True, "getFilename", "", "", "Argument[this]", "get-resource", "manual", "unsafe-url-forward"]
- - ["org.springframework.core.io", "ClassPathResource", True, "getPath", "", "", "Argument[this]", "get-resource", "manual", "unsafe-url-forward"]
- - ["org.springframework.core.io", "ClassPathResource", True, "getURL", "", "", "Argument[this]", "get-resource", "manual", "unsafe-url-forward"]
- - ["org.springframework.core.io", "ClassPathResource", True, "resolveURL", "", "", "Argument[this]", "get-resource", "manual", "unsafe-url-forward"]
- - addsTo:
- pack: codeql/java-all
- extensible: experimentalSummaryModel
- data:
- - ["org.springframework.core.io", "ClassPathResource", False, "ClassPathResource", "", "", "Argument[0]", "Argument[this]", "taint", "manual", "unsafe-url-forward"]
- - ["org.springframework.core.io", "Resource", True, "createRelative", "", "", "Argument[0]", "ReturnValue", "taint", "manual", "unsafe-url-forward"]
- - ["org.springframework.core.io", "ResourceLoader", True, "getResource", "", "", "Argument[0]", "ReturnValue", "taint", "manual", "unsafe-url-forward"]
diff --git a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeLoadSpringResource.java b/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeLoadSpringResource.java
deleted file mode 100644
index ce462fe490ef..000000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeLoadSpringResource.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeResourceGet.java b/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeResourceGet.java
deleted file mode 100644
index 8b3583bf59e2..000000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeResourceGet.java
+++ /dev/null
@@ -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());
-}
diff --git a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java b/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java
deleted file mode 100644
index 88a794ab9c64..000000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeServletRequestDispatch.java
+++ /dev/null
@@ -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("%")) { ... }
\ No newline at end of file
diff --git a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.java b/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.java
deleted file mode 100644
index d159c4057362..000000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.java
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qhelp b/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qhelp
deleted file mode 100644
index 2e425952edc3..000000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qhelp
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
-
-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.
-
-
-
-
-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 user_content/) or disallowed
-(e.g. must not come within /internal) paths, ensuring that neither path traversal using ../
-or URL encoding are used to evade these checks.
-
-
-
-
-
-The following examples show the bad case and the good case respectively.
-The bad methods show an HTTP request parameter being used directly in a URL forward
-without validating the input, which may cause file leakage. In the good1 method,
-ordinary forwarding requests are shown, which will not cause file leakage.
-
-
-
-
-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.
-
-
-
-
-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.
-
-
-
-
-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.
-
-
-
-
-
-File Disclosure:
- Unsafe Url Forward.
-
-Jakarta Javadoc:
- Security vulnerability with unsafe usage of RequestDispatcher.
-
-Micro Focus:
- File Disclosure: J2EE
-
-CVE-2015-5174:
- Apache Tomcat 6.0/7.0/8.0/9.0 Servletcontext getResource/getResourceAsStream/getResourcePaths Path Traversal
-
-CVE-2019-3799:
- CVE-2019-3799 - Spring-Cloud-Config-Server Directory Traversal < 2.1.2, 2.0.4, 1.4.6
-
-
-
diff --git a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.ql b/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.ql
deleted file mode 100644
index 15dd04a0a76f..000000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.ql
+++ /dev/null
@@ -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;
-
-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"
diff --git a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qll b/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qll
deleted file mode 100644
index 1baec2dd1fa5..000000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qll
+++ /dev/null
@@ -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()
- }
-}
From a8eb1d10f646c0928dfb97b29d7fe418aadb8bab Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 17 Mar 2024 22:35:27 -0400
Subject: [PATCH 35/40] Java: remove experimental tests
---
.../CWE-552/UnsafeLoadSpringResource.java | 155 ----------
.../security/CWE-552/UnsafeRequestPath.java | 52 ----
.../security/CWE-552/UnsafeResourceGet.java | 270 ------------------
.../security/CWE-552/UnsafeResourceGet2.java | 58 ----
.../CWE-552/UnsafeServletRequestDispatch.java | 131 ---------
.../CWE-552/UnsafeUrlForward.expected | 129 ---------
.../security/CWE-552/UnsafeUrlForward.java | 78 -----
.../security/CWE-552/UnsafeUrlForward.qlref | 1 -
.../query-tests/security/CWE-552/options | 1 -
9 files changed, 875 deletions(-)
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-552/UnsafeRequestPath.java
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-552/UnsafeResourceGet.java
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-552/UnsafeResourceGet2.java
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.expected
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.java
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.qlref
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-552/options
diff --git a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeLoadSpringResource.java b/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
deleted file mode 100644
index c7e114aede35..000000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeLoadSpringResource.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package com.example;
-
-import java.io.File;
-import java.io.FileReader;
-import java.io.InputStreamReader;
-import java.io.IOException;
-import java.io.Reader;
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.nio.file.Files;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.core.io.ClassPathResource;
-import org.springframework.core.io.Resource;
-import org.springframework.core.io.ResourceLoader;
-import org.springframework.util.ResourceUtils;
-import org.springframework.util.StringUtils;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-/** Sample class of Spring RestController */
-@RestController
-public class UnsafeLoadSpringResource {
- @GetMapping("/file1")
- //BAD: Get resource from ClassPathResource without input validation
- public String getFileContent1(@RequestParam(name="fileName") String fileName) {
- // A request such as the following can disclose source code and application configuration
- // fileName=/../../WEB-INF/views/page.jsp
- // fileName=/com/example/package/SampleController.class
- ClassPathResource clr = new ClassPathResource(fileName);
- char[] buffer = new char[4096];
- StringBuilder out = new StringBuilder();
- try {
- Reader in = new FileReader(clr.getFilename());
- for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
- out.append(buffer, 0, numRead);
- }
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- return out.toString();
- }
-
- @GetMapping("/file1a")
- //GOOD: Get resource from ClassPathResource with input path validation
- public String getFileContent1a(@RequestParam(name="fileName") String fileName) {
- String result = null;
- if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
- ClassPathResource clr = new ClassPathResource(fileName);
- char[] buffer = new char[4096];
- StringBuilder out = new StringBuilder();
- try {
- Reader in = new InputStreamReader(clr.getInputStream(), "UTF-8");
- for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
- out.append(buffer, 0, numRead);
- }
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- result = out.toString();
- }
- return result;
- }
-
- @GetMapping("/file2")
- //BAD: Get resource from ResourceUtils without input validation
- public String getFileContent2(@RequestParam(name="fileName") String fileName) {
- String content = null;
-
- try {
- // A request such as the following can disclose source code and system configuration
- // fileName=/etc/hosts
- // fileName=file:/etc/hosts
- // fileName=/opt/appdir/WEB-INF/views/page.jsp
- File file = ResourceUtils.getFile(fileName);
- //Read File Content
- content = new String(Files.readAllBytes(file.toPath()));
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- return content;
- }
-
- @GetMapping("/file2a")
- //GOOD: Get resource from ResourceUtils with input path validation
- public String getFileContent2a(@RequestParam(name="fileName") String fileName) {
- String content = null;
-
- if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
- try {
- File file = ResourceUtils.getFile(fileName);
- //Read File Content
- content = new String(Files.readAllBytes(file.toPath()));
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- }
- return content;
- }
-
- @Autowired
- ResourceLoader resourceLoader;
-
- @GetMapping("/file3")
- //BAD: Get resource from ResourceLoader (same as application context) without input validation
- // Note it is not detected without the generic `resource.getInputStream()` check
- public String getFileContent3(@RequestParam(name="fileName") String fileName) {
- String content = null;
-
- try {
- // A request such as the following can disclose source code and system configuration
- // fileName=/WEB-INF/views/page.jsp
- // fileName=/WEB-INF/classes/com/example/package/SampleController.class
- // fileName=file:/etc/hosts
- Resource resource = resourceLoader.getResource(fileName);
-
- char[] buffer = new char[4096];
- StringBuilder out = new StringBuilder();
-
- Reader in = new InputStreamReader(resource.getInputStream(), "UTF-8");
- for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
- out.append(buffer, 0, numRead);
- }
- content = out.toString();
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- return content;
- }
-
- @GetMapping("/file3a")
- //GOOD: Get resource from ResourceLoader (same as application context) with input path validation
- public String getFileContent3a(@RequestParam(name="fileName") String fileName) {
- String content = null;
-
- if (fileName.startsWith("/safe_dir") && !fileName.contains("..")) {
- try {
- Resource resource = resourceLoader.getResource(fileName);
-
- char[] buffer = new char[4096];
- StringBuilder out = new StringBuilder();
-
- Reader in = new InputStreamReader(resource.getInputStream(), "UTF-8");
- for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
- out.append(buffer, 0, numRead);
- }
- content = out.toString();
- } catch (IOException ie) {
- ie.printStackTrace();
- }
- }
- return content;
- }
-}
diff --git a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeRequestPath.java b/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeRequestPath.java
deleted file mode 100644
index 2de0cae0d3c5..000000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeRequestPath.java
+++ /dev/null
@@ -1,52 +0,0 @@
-import java.io.IOException;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-// @WebFilter("/*")
-public class UnsafeRequestPath implements Filter {
- private static final String BASE_PATH = "/pages";
-
- @Override
- // BAD: Request dispatcher from servlet path without check
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- String path = ((HttpServletRequest) request).getServletPath();
- // A sample payload "/%57EB-INF/web.xml" can bypass this `startsWith` check
- if (path != null && !path.startsWith("/WEB-INF")) {
- request.getRequestDispatcher(path).forward(request, response);
- } else {
- chain.doFilter(request, response);
- }
- }
-
- // GOOD: Request dispatcher from servlet path with check
- public void doFilter2(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- String path = ((HttpServletRequest) request).getServletPath();
-
- if (path.startsWith(BASE_PATH) && !path.contains("..")) {
- request.getRequestDispatcher(path).forward(request, response);
- } else {
- chain.doFilter(request, response);
- }
- }
-
- // GOOD: Request dispatcher from servlet path with whitelisted string comparison
- public void doFilter3(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- String path = ((HttpServletRequest) request).getServletPath();
-
- if (path.equals("/comaction")) {
- request.getRequestDispatcher(path).forward(request, response);
- } else {
- chain.doFilter(request, response);
- }
- }
-}
diff --git a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeResourceGet.java b/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeResourceGet.java
deleted file mode 100644
index 64c23334f187..000000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeResourceGet.java
+++ /dev/null
@@ -1,270 +0,0 @@
-package com.example;
-
-import java.io.InputStream;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.net.URI;
-import java.net.URL;
-import java.net.URISyntaxException;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.ServletOutputStream;
-import javax.servlet.ServletException;
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletContext;
-
-import io.undertow.server.handlers.resource.FileResourceManager;
-import io.undertow.server.handlers.resource.Resource;
-import org.jboss.vfs.VFS;
-import org.jboss.vfs.VirtualFile;
-
-public class UnsafeResourceGet extends HttpServlet {
- private static final String BASE_PATH = "/pages";
-
- @Override
- // BAD: getResource constructed from `ServletContext` without input validation
- protected void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- ServletConfig cfg = getServletConfig();
- ServletContext sc = cfg.getServletContext();
-
- // A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
- URL url = sc.getResource(requestUrl);
-
- InputStream in = url.openStream();
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- // GOOD: getResource constructed from `ServletContext` with input validation
- protected void doGetGood(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- ServletConfig cfg = getServletConfig();
- ServletContext sc = cfg.getServletContext();
-
- Path path = Paths.get(requestUrl).normalize().toRealPath();
- if (path.startsWith(BASE_PATH)) {
- URL url = sc.getResource(path.toString());
-
- InputStream in = url.openStream();
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
- }
-
- // GOOD: getResource constructed from `ServletContext` with null check only
- protected void doGetGood2(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- PrintWriter writer = response.getWriter();
-
- ServletConfig cfg = getServletConfig();
- ServletContext sc = cfg.getServletContext();
-
- // A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
- URL url = sc.getResource(requestUrl);
- if (url == null) {
- writer.println("Requested source not found");
- }
- }
-
- // GOOD: getResource constructed from `ServletContext` with `equals` check
- protected void doGetGood3(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- ServletContext sc = request.getServletContext();
-
- if (requestUrl.equals("/public/crossdomain.xml")) {
- URL url = sc.getResource(requestUrl);
-
- InputStream in = url.openStream();
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
- }
-
- @Override
- // BAD: getResourceAsStream constructed from `ServletContext` without input validation
- protected void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
- ServletOutputStream out = response.getOutputStream();
-
- // A sample request /fake.jsp/../WEB-INF/web.xml can load the web.xml file
- InputStream in = request.getServletContext().getResourceAsStream(requestPath);
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- // GOOD: getResourceAsStream constructed from `ServletContext` with input validation
- protected void doPostGood(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
- ServletOutputStream out = response.getOutputStream();
-
- if (!requestPath.contains("..") && requestPath.startsWith("/trusted")) {
- InputStream in = request.getServletContext().getResourceAsStream(requestPath);
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
- }
-
- @Override
- // BAD: getResource constructed from `Class` without input validation
- protected void doHead(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- // A sample request /fake.jsp/../../../WEB-INF/web.xml can load the web.xml file
- // Note the class is in two levels of subpackages and `Class.getResource` starts from its own directory
- URL url = getClass().getResource(requestUrl);
-
- InputStream in = url.openStream();
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- // GOOD: getResource constructed from `Class` with input validation
- protected void doHeadGood(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- Path path = Paths.get(requestUrl).normalize().toRealPath();
- if (path.startsWith(BASE_PATH)) {
- URL url = getClass().getResource(path.toString());
-
- InputStream in = url.openStream();
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
- }
-
- @Override
- // BAD: getResourceAsStream constructed from `ClassLoader` without input validation
- protected void doPut(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
- ServletOutputStream out = response.getOutputStream();
-
- ServletConfig cfg = getServletConfig();
- ServletContext sc = cfg.getServletContext();
-
- // A sample request /fake.jsp/../../../WEB-INF/web.xml can load the web.xml file
- // Note the class is in two levels of subpackages and `ClassLoader.getResourceAsStream` starts from its own directory
- InputStream in = getClass().getClassLoader().getResourceAsStream(requestPath);
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- // GOOD: getResourceAsStream constructed from `ClassLoader` with input validation
- protected void doPutGood(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
- ServletOutputStream out = response.getOutputStream();
-
- ServletConfig cfg = getServletConfig();
- ServletContext sc = cfg.getServletContext();
-
- if (!requestPath.contains("..") && requestPath.startsWith("/trusted")) {
- InputStream in = getClass().getClassLoader().getResourceAsStream(requestPath);
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
- }
-
- // BAD: getResource constructed from `ClassLoader` without input validation
- protected void doPutBad(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestUrl = request.getParameter("requestURL");
- ServletOutputStream out = response.getOutputStream();
-
- // A sample request /fake.jsp/../../../WEB-INF/web.xml can load the web.xml file
- // Note the class is in two levels of subpackages and `ClassLoader.getResource` starts from its own directory
- URL url = getClass().getClassLoader().getResource(requestUrl);
-
- InputStream in = url.openStream();
- byte[] buf = new byte[4 * 1024]; // 4K buffer
- int bytesRead;
- while ((bytesRead = in.read(buf)) != -1) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- // BAD: getResource constructed using Undertow IO without input validation
- protected void doPutBad2(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
-
- try {
- FileResourceManager rm = new FileResourceManager(VFS.getChild(new URI("/usr/share")).getPhysicalFile());
- Resource rs = rm.getResource(requestPath);
-
- VirtualFile overlay = VFS.getChild(new URI("EAP_HOME/modules/"));
- // Do file operations
- overlay.getChild(rs.getPath());
- } catch (URISyntaxException ue) {
- throw new IOException("Cannot parse the URI");
- }
- }
-
- // GOOD: getResource constructed using Undertow IO with input validation
- protected void doPutGood2(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String requestPath = request.getParameter("requestPath");
-
- try {
- FileResourceManager rm = new FileResourceManager(VFS.getChild(new URI("/usr/share")).getPhysicalFile());
- Resource rs = rm.getResource(requestPath);
-
- VirtualFile overlay = VFS.getChild(new URI("EAP_HOME/modules/"));
- String path = rs.getPath();
- if (path.startsWith("/trusted_path") && !path.contains("..")) {
- // Do file operations
- overlay.getChild(path);
- }
- } catch (URISyntaxException ue) {
- throw new IOException("Cannot parse the URI");
- }
- }
-}
diff --git a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeResourceGet2.java b/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeResourceGet2.java
deleted file mode 100644
index b3d041d024cf..000000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeResourceGet2.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.example;
-
-import javax.faces.context.FacesContext;
-import java.io.BufferedReader;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.IOException;
-import java.net.URL;
-import java.util.Map;
-
-/** Sample class of JSF managed bean */
-public class UnsafeResourceGet2 {
- // BAD: getResourceAsStream constructed from `ExternalContext` without input validation
- public String parameterActionBad1() throws IOException {
- FacesContext fc = FacesContext.getCurrentInstance();
- Map params = fc.getExternalContext().getRequestParameterMap();
- String loadUrl = params.get("loadUrl");
-
- InputStreamReader isr = new InputStreamReader(fc.getExternalContext().getResourceAsStream(loadUrl));
- BufferedReader br = new BufferedReader(isr);
- if(br.ready()) {
- //Do Stuff
- return "result";
- }
-
- return "home";
- }
-
- // BAD: getResource constructed from `ExternalContext` without input validation
- public String parameterActionBad2() throws IOException {
- FacesContext fc = FacesContext.getCurrentInstance();
- Map params = fc.getExternalContext().getRequestParameterMap();
- String loadUrl = params.get("loadUrl");
-
- URL url = fc.getExternalContext().getResource(loadUrl);
-
- InputStream in = url.openStream();
- //Do Stuff
- return "result";
- }
-
- // GOOD: getResource constructed from `ExternalContext` with input validation
- public String parameterActionGood1() throws IOException {
- FacesContext fc = FacesContext.getCurrentInstance();
- Map params = fc.getExternalContext().getRequestParameterMap();
- String loadUrl = params.get("loadUrl");
-
- if (loadUrl.equals("/public/crossdomain.xml")) {
- URL url = fc.getExternalContext().getResource(loadUrl);
-
- InputStream in = url.openStream();
- //Do Stuff
- return "result";
- }
-
- return "home";
- }
-}
diff --git a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java b/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
deleted file mode 100644
index ee63939b209e..000000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java
+++ /dev/null
@@ -1,131 +0,0 @@
-import java.io.IOException;
-import java.net.URLDecoder;
-import java.io.File;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.RequestDispatcher;
-import javax.servlet.ServletException;
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletContext;
-
-public class UnsafeServletRequestDispatch extends HttpServlet {
- private static final String BASE_PATH = "/pages";
-
- @Override
- // BAD: Request dispatcher constructed from `ServletContext` without input validation
- protected void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String action = request.getParameter("action");
- String returnURL = request.getParameter("returnURL");
-
- ServletConfig cfg = getServletConfig();
- if (action.equals("Login")) {
- ServletContext sc = cfg.getServletContext();
- RequestDispatcher rd = sc.getRequestDispatcher("/Login.jsp");
- rd.forward(request, response);
- } else {
- ServletContext sc = cfg.getServletContext();
- RequestDispatcher rd = sc.getRequestDispatcher(returnURL);
- rd.forward(request, response);
- }
- }
-
- @Override
- // BAD: Request dispatcher constructed from `HttpServletRequest` without input validation
- protected void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String action = request.getParameter("action");
- String returnURL = request.getParameter("returnURL");
-
- if (action.equals("Login")) {
- RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
- rd.forward(request, response);
- } else {
- RequestDispatcher rd = request.getRequestDispatcher(returnURL);
- rd.forward(request, response);
- }
- }
-
- @Override
- // GOOD: Request dispatcher with a whitelisted URI
- protected void doPut(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String action = request.getParameter("action");
-
- if (action.equals("Login")) {
- RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
- rd.forward(request, response);
- } else if (action.equals("Register")) {
- RequestDispatcher rd = request.getRequestDispatcher("/Register.jsp");
- rd.forward(request, response);
- }
- }
-
- // BAD: Request dispatcher without path traversal check
- protected void doHead2(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String path = request.getParameter("path");
-
- // A sample payload "/pages/welcome.jsp/../WEB-INF/web.xml" can bypass the `startsWith` check
- // The payload "/pages/welcome.jsp/../../%57EB-INF/web.xml" can bypass the check as well since RequestDispatcher will decode `%57` as `W`
- if (path.startsWith(BASE_PATH)) {
- request.getServletContext().getRequestDispatcher(path).include(request, response);
- }
- }
-
- // GOOD: Request dispatcher with path traversal check
- protected void doHead3(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String path = request.getParameter("path");
-
- if (path.startsWith(BASE_PATH) && !path.contains("..")) {
- request.getServletContext().getRequestDispatcher(path).include(request, response);
- }
- }
-
- // GOOD: Request dispatcher with path normalization and comparison
- protected void doHead4(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String path = request.getParameter("path");
- Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
-
- // /pages/welcome.jsp/../../WEB-INF/web.xml becomes /WEB-INF/web.xml
- // /pages/welcome.jsp/../../%57EB-INF/web.xml becomes /%57EB-INF/web.xml
- if (requestedPath.startsWith(BASE_PATH)) {
- request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response);
- }
- }
-
- // FN: Request dispatcher with negation check and path normalization, but without URL decoding
- // When promoting this query, consider using FlowStates to make `getRequestDispatcher` a sink
- // only if a URL-decoding step has NOT been crossed (i.e. make URLDecoder.decode change the
- // state to a different value than the one required at the sink).
- protected void doHead5(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String path = request.getParameter("path");
- Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
-
- if (!requestedPath.startsWith("/WEB-INF") && !requestedPath.startsWith("/META-INF")) {
- request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response);
- }
- }
-
- // GOOD: Request dispatcher with path traversal check and URL decoding
- protected void doHead6(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String path = request.getParameter("path");
- boolean hasEncoding = path.contains("%");
- while (hasEncoding) {
- path = URLDecoder.decode(path, "UTF-8");
- hasEncoding = path.contains("%");
- }
-
- if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- request.getServletContext().getRequestDispatcher(path).include(request, response);
- }
- }
-}
diff --git a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.expected b/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.expected
deleted file mode 100644
index 545471868e76..000000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.expected
+++ /dev/null
@@ -1,129 +0,0 @@
-edges
-| UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String | provenance | |
-| UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource | UnsafeLoadSpringResource.java:35:31:35:33 | clr | provenance | |
-| UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String | UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource | provenance | |
-| UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | UnsafeLoadSpringResource.java:76:38:76:45 | fileName | provenance | |
-| UnsafeLoadSpringResource.java:108:32:108:77 | fileName : String | UnsafeLoadSpringResource.java:116:51:116:58 | fileName | provenance | |
-| UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | UnsafeRequestPath.java:23:33:23:36 | path | provenance | |
-| UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:17:20:17:25 | params : Map | provenance | |
-| UnsafeResourceGet2.java:17:20:17:25 | params : Map | UnsafeResourceGet2.java:17:20:17:40 | get(...) : String | provenance | |
-| UnsafeResourceGet2.java:17:20:17:40 | get(...) : String | UnsafeResourceGet2.java:19:93:19:99 | loadUrl | provenance | |
-| UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:33:20:33:25 | params : Map | provenance | |
-| UnsafeResourceGet2.java:33:20:33:25 | params : Map | UnsafeResourceGet2.java:33:20:33:40 | get(...) : String | provenance | |
-| UnsafeResourceGet2.java:33:20:33:40 | get(...) : String | UnsafeResourceGet2.java:35:49:35:55 | loadUrl : String | provenance | |
-| UnsafeResourceGet2.java:35:13:35:56 | getResource(...) : URL | UnsafeResourceGet2.java:37:20:37:22 | url | provenance | |
-| UnsafeResourceGet2.java:35:49:35:55 | loadUrl : String | UnsafeResourceGet2.java:35:13:35:56 | getResource(...) : URL | provenance | |
-| UnsafeResourceGet.java:32:23:32:56 | getParameter(...) : String | UnsafeResourceGet.java:39:28:39:37 | requestUrl : String | provenance | |
-| UnsafeResourceGet.java:39:13:39:38 | getResource(...) : URL | UnsafeResourceGet.java:41:20:41:22 | url | provenance | |
-| UnsafeResourceGet.java:39:28:39:37 | requestUrl : String | UnsafeResourceGet.java:39:13:39:38 | getResource(...) : URL | provenance | |
-| UnsafeResourceGet.java:111:24:111:58 | getParameter(...) : String | UnsafeResourceGet.java:115:68:115:78 | requestPath | provenance | |
-| UnsafeResourceGet.java:143:23:143:56 | getParameter(...) : String | UnsafeResourceGet.java:148:36:148:45 | requestUrl : String | provenance | |
-| UnsafeResourceGet.java:148:13:148:46 | getResource(...) : URL | UnsafeResourceGet.java:150:20:150:22 | url | provenance | |
-| UnsafeResourceGet.java:148:36:148:45 | requestUrl : String | UnsafeResourceGet.java:148:13:148:46 | getResource(...) : URL | provenance | |
-| UnsafeResourceGet.java:181:24:181:58 | getParameter(...) : String | UnsafeResourceGet.java:189:68:189:78 | requestPath | provenance | |
-| UnsafeResourceGet.java:219:23:219:56 | getParameter(...) : String | UnsafeResourceGet.java:224:53:224:62 | requestUrl : String | provenance | |
-| UnsafeResourceGet.java:224:13:224:63 | getResource(...) : URL | UnsafeResourceGet.java:226:20:226:22 | url | provenance | |
-| UnsafeResourceGet.java:224:53:224:62 | requestUrl : String | UnsafeResourceGet.java:224:13:224:63 | getResource(...) : URL | provenance | |
-| UnsafeResourceGet.java:237:24:237:58 | getParameter(...) : String | UnsafeResourceGet.java:241:33:241:43 | requestPath : String | provenance | |
-| UnsafeResourceGet.java:241:18:241:44 | getResource(...) : Resource | UnsafeResourceGet.java:245:21:245:22 | rs : Resource | provenance | |
-| UnsafeResourceGet.java:241:33:241:43 | requestPath : String | UnsafeResourceGet.java:241:18:241:44 | getResource(...) : Resource | provenance | |
-| UnsafeResourceGet.java:245:21:245:22 | rs : Resource | UnsafeResourceGet.java:245:21:245:32 | getPath(...) | provenance | |
-| UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | provenance | |
-| UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | provenance | |
-| UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | UnsafeServletRequestDispatch.java:76:53:76:56 | path | provenance | |
-| UnsafeUrlForward.java:13:27:13:36 | url : String | UnsafeUrlForward.java:14:27:14:29 | url | provenance | |
-| UnsafeUrlForward.java:18:27:18:36 | url : String | UnsafeUrlForward.java:20:28:20:30 | url | provenance | |
-| UnsafeUrlForward.java:25:21:25:30 | url : String | UnsafeUrlForward.java:26:23:26:25 | url | provenance | |
-| UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:48:31:63 | ... + ... | provenance | |
-| UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:61:31:63 | url | provenance | |
-| UnsafeUrlForward.java:36:19:36:28 | url : String | UnsafeUrlForward.java:38:33:38:35 | url | provenance | |
-| UnsafeUrlForward.java:47:19:47:28 | url : String | UnsafeUrlForward.java:49:33:49:62 | ... + ... | provenance | |
-| UnsafeUrlForward.java:58:19:58:28 | url : String | UnsafeUrlForward.java:60:33:60:62 | ... + ... | provenance | |
-nodes
-| UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | semmle.label | fileName : String |
-| UnsafeLoadSpringResource.java:31:27:31:57 | new ClassPathResource(...) : ClassPathResource | semmle.label | new ClassPathResource(...) : ClassPathResource |
-| UnsafeLoadSpringResource.java:31:49:31:56 | fileName : String | semmle.label | fileName : String |
-| UnsafeLoadSpringResource.java:35:31:35:33 | clr | semmle.label | clr |
-| UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | semmle.label | fileName : String |
-| UnsafeLoadSpringResource.java:76:38:76:45 | fileName | semmle.label | fileName |
-| UnsafeLoadSpringResource.java:108:32:108:77 | fileName : String | semmle.label | fileName : String |
-| UnsafeLoadSpringResource.java:116:51:116:58 | fileName | semmle.label | fileName |
-| UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | semmle.label | getServletPath(...) : String |
-| UnsafeRequestPath.java:23:33:23:36 | path | semmle.label | path |
-| UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | semmle.label | getRequestParameterMap(...) : Map |
-| UnsafeResourceGet2.java:17:20:17:25 | params : Map | semmle.label | params : Map |
-| UnsafeResourceGet2.java:17:20:17:40 | get(...) : String | semmle.label | get(...) : String |
-| UnsafeResourceGet2.java:19:93:19:99 | loadUrl | semmle.label | loadUrl |
-| UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) : Map | semmle.label | getRequestParameterMap(...) : Map |
-| UnsafeResourceGet2.java:33:20:33:25 | params : Map | semmle.label | params : Map |
-| UnsafeResourceGet2.java:33:20:33:40 | get(...) : String | semmle.label | get(...) : String |
-| UnsafeResourceGet2.java:35:13:35:56 | getResource(...) : URL | semmle.label | getResource(...) : URL |
-| UnsafeResourceGet2.java:35:49:35:55 | loadUrl : String | semmle.label | loadUrl : String |
-| UnsafeResourceGet2.java:37:20:37:22 | url | semmle.label | url |
-| UnsafeResourceGet.java:32:23:32:56 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:39:13:39:38 | getResource(...) : URL | semmle.label | getResource(...) : URL |
-| UnsafeResourceGet.java:39:28:39:37 | requestUrl : String | semmle.label | requestUrl : String |
-| UnsafeResourceGet.java:41:20:41:22 | url | semmle.label | url |
-| UnsafeResourceGet.java:111:24:111:58 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:115:68:115:78 | requestPath | semmle.label | requestPath |
-| UnsafeResourceGet.java:143:23:143:56 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:148:13:148:46 | getResource(...) : URL | semmle.label | getResource(...) : URL |
-| UnsafeResourceGet.java:148:36:148:45 | requestUrl : String | semmle.label | requestUrl : String |
-| UnsafeResourceGet.java:150:20:150:22 | url | semmle.label | url |
-| UnsafeResourceGet.java:181:24:181:58 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:189:68:189:78 | requestPath | semmle.label | requestPath |
-| UnsafeResourceGet.java:219:23:219:56 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:224:13:224:63 | getResource(...) : URL | semmle.label | getResource(...) : URL |
-| UnsafeResourceGet.java:224:53:224:62 | requestUrl : String | semmle.label | requestUrl : String |
-| UnsafeResourceGet.java:226:20:226:22 | url | semmle.label | url |
-| UnsafeResourceGet.java:237:24:237:58 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeResourceGet.java:241:18:241:44 | getResource(...) : Resource | semmle.label | getResource(...) : Resource |
-| UnsafeResourceGet.java:241:33:241:43 | requestPath : String | semmle.label | requestPath : String |
-| UnsafeResourceGet.java:245:21:245:22 | rs : Resource | semmle.label | rs : Resource |
-| UnsafeResourceGet.java:245:21:245:32 | getPath(...) | semmle.label | getPath(...) |
-| UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | semmle.label | returnURL |
-| UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | semmle.label | returnURL |
-| UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| UnsafeServletRequestDispatch.java:76:53:76:56 | path | semmle.label | path |
-| UnsafeUrlForward.java:13:27:13:36 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:14:27:14:29 | url | semmle.label | url |
-| UnsafeUrlForward.java:18:27:18:36 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:20:28:20:30 | url | semmle.label | url |
-| UnsafeUrlForward.java:25:21:25:30 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:26:23:26:25 | url | semmle.label | url |
-| UnsafeUrlForward.java:30:27:30:36 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:31:48:31:63 | ... + ... | semmle.label | ... + ... |
-| UnsafeUrlForward.java:31:61:31:63 | url | semmle.label | url |
-| UnsafeUrlForward.java:36:19:36:28 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:38:33:38:35 | url | semmle.label | url |
-| UnsafeUrlForward.java:47:19:47:28 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:49:33:49:62 | ... + ... | semmle.label | ... + ... |
-| UnsafeUrlForward.java:58:19:58:28 | url : String | semmle.label | url : String |
-| UnsafeUrlForward.java:60:33:60:62 | ... + ... | semmle.label | ... + ... |
-subpaths
-#select
-| UnsafeLoadSpringResource.java:35:31:35:33 | clr | UnsafeLoadSpringResource.java:27:32:27:77 | fileName : String | UnsafeLoadSpringResource.java:35:31:35:33 | clr | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:27:32:27:77 | fileName | user-provided value |
-| UnsafeLoadSpringResource.java:76:38:76:45 | fileName | UnsafeLoadSpringResource.java:68:32:68:77 | fileName : String | UnsafeLoadSpringResource.java:76:38:76:45 | fileName | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:68:32:68:77 | fileName | user-provided value |
-| UnsafeLoadSpringResource.java:116:51:116:58 | fileName | UnsafeLoadSpringResource.java:108:32:108:77 | fileName : String | UnsafeLoadSpringResource.java:116:51:116:58 | fileName | Potentially untrusted URL forward due to $@. | UnsafeLoadSpringResource.java:108:32:108:77 | fileName | user-provided value |
-| UnsafeRequestPath.java:23:33:23:36 | path | UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) : String | UnsafeRequestPath.java:23:33:23:36 | path | Potentially untrusted URL forward due to $@. | UnsafeRequestPath.java:20:17:20:63 | getServletPath(...) | user-provided value |
-| UnsafeResourceGet2.java:19:93:19:99 | loadUrl | UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:19:93:19:99 | loadUrl | Potentially untrusted URL forward due to $@. | UnsafeResourceGet2.java:16:32:16:79 | getRequestParameterMap(...) | user-provided value |
-| UnsafeResourceGet2.java:37:20:37:22 | url | UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) : Map | UnsafeResourceGet2.java:37:20:37:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet2.java:32:32:32:79 | getRequestParameterMap(...) | user-provided value |
-| UnsafeResourceGet.java:41:20:41:22 | url | UnsafeResourceGet.java:32:23:32:56 | getParameter(...) : String | UnsafeResourceGet.java:41:20:41:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:32:23:32:56 | getParameter(...) | user-provided value |
-| UnsafeResourceGet.java:115:68:115:78 | requestPath | UnsafeResourceGet.java:111:24:111:58 | getParameter(...) : String | UnsafeResourceGet.java:115:68:115:78 | requestPath | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:111:24:111:58 | getParameter(...) | user-provided value |
-| UnsafeResourceGet.java:150:20:150:22 | url | UnsafeResourceGet.java:143:23:143:56 | getParameter(...) : String | UnsafeResourceGet.java:150:20:150:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:143:23:143:56 | getParameter(...) | user-provided value |
-| UnsafeResourceGet.java:189:68:189:78 | requestPath | UnsafeResourceGet.java:181:24:181:58 | getParameter(...) : String | UnsafeResourceGet.java:189:68:189:78 | requestPath | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:181:24:181:58 | getParameter(...) | user-provided value |
-| UnsafeResourceGet.java:226:20:226:22 | url | UnsafeResourceGet.java:219:23:219:56 | getParameter(...) : String | UnsafeResourceGet.java:226:20:226:22 | url | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:219:23:219:56 | getParameter(...) | user-provided value |
-| UnsafeResourceGet.java:245:21:245:32 | getPath(...) | UnsafeResourceGet.java:237:24:237:58 | getParameter(...) : String | UnsafeResourceGet.java:245:21:245:32 | getPath(...) | Potentially untrusted URL forward due to $@. | UnsafeResourceGet.java:237:24:237:58 | getParameter(...) | user-provided value |
-| UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) | user-provided value |
-| UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) | user-provided value |
-| UnsafeServletRequestDispatch.java:76:53:76:56 | path | UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | UnsafeServletRequestDispatch.java:76:53:76:56 | path | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) | user-provided value |
-| UnsafeUrlForward.java:14:27:14:29 | url | UnsafeUrlForward.java:13:27:13:36 | url : String | UnsafeUrlForward.java:14:27:14:29 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:13:27:13:36 | url | user-provided value |
-| UnsafeUrlForward.java:20:28:20:30 | url | UnsafeUrlForward.java:18:27:18:36 | url : String | UnsafeUrlForward.java:20:28:20:30 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:18:27:18:36 | url | user-provided value |
-| UnsafeUrlForward.java:26:23:26:25 | url | UnsafeUrlForward.java:25:21:25:30 | url : String | UnsafeUrlForward.java:26:23:26:25 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:25:21:25:30 | url | user-provided value |
-| UnsafeUrlForward.java:31:48:31:63 | ... + ... | UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:48:31:63 | ... + ... | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:30:27:30:36 | url | user-provided value |
-| UnsafeUrlForward.java:31:61:31:63 | url | UnsafeUrlForward.java:30:27:30:36 | url : String | UnsafeUrlForward.java:31:61:31:63 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:30:27:30:36 | url | user-provided value |
-| UnsafeUrlForward.java:38:33:38:35 | url | UnsafeUrlForward.java:36:19:36:28 | url : String | UnsafeUrlForward.java:38:33:38:35 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:36:19:36:28 | url | user-provided value |
-| UnsafeUrlForward.java:49:33:49:62 | ... + ... | UnsafeUrlForward.java:47:19:47:28 | url : String | UnsafeUrlForward.java:49:33:49:62 | ... + ... | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:47:19:47:28 | url | user-provided value |
-| UnsafeUrlForward.java:60:33:60:62 | ... + ... | UnsafeUrlForward.java:58:19:58:28 | url : String | UnsafeUrlForward.java:60:33:60:62 | ... + ... | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:58:19:58:28 | url | user-provided value |
diff --git a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.java b/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.java
deleted file mode 100644
index 4018ed289481..000000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.java
+++ /dev/null
@@ -1,78 +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 ModelAndView bad2(String url) {
- ModelAndView modelAndView = new ModelAndView();
- modelAndView.setViewName(url);
- return modelAndView;
- }
-
- @GetMapping("/bad3")
- public String bad3(String url) {
- return "forward:" + url + "/swagger-ui/index.html";
- }
-
- @GetMapping("/bad4")
- public ModelAndView bad4(String url) {
- ModelAndView modelAndView = new ModelAndView("forward:" + url);
- return modelAndView;
- }
-
- @GetMapping("/bad5")
- public void bad5(String url, HttpServletRequest request, HttpServletResponse response) {
- try {
- request.getRequestDispatcher(url).include(request, response);
- } catch (ServletException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- @GetMapping("/bad6")
- public void bad6(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("/bad7")
- public void bad7(String url, HttpServletRequest request, HttpServletResponse response) {
- try {
- request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").forward(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();
- }
- }
-}
diff --git a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.qlref b/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.qlref
deleted file mode 100644
index 2e4cb5e726a9..000000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.qlref
+++ /dev/null
@@ -1 +0,0 @@
-experimental/Security/CWE/CWE-552/UnsafeUrlForward.ql
\ No newline at end of file
diff --git a/java/ql/test/experimental/query-tests/security/CWE-552/options b/java/ql/test/experimental/query-tests/security/CWE-552/options
deleted file mode 100644
index 8fbf23e17dff..000000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-552/options
+++ /dev/null
@@ -1 +0,0 @@
-//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/springframework-5.3.8/:${testdir}/../../../../stubs/javax-faces-2.3/:${testdir}/../../../../stubs/undertow-io-2.2/:${testdir}/../../../../stubs/jboss-vfs-3.2/:${testdir}/../../../../stubs/springframework-5.3.8/
From 35fbc95cc743f63b16a68f8f5f6ce82a9d0bdbeb Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 27 Mar 2024 08:09:40 -0400
Subject: [PATCH 36/40] Java: remove redundant line
---
java/ql/lib/semmle/code/java/security/UrlForward.qll | 1 -
1 file changed, 1 deletion(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
index 464a125ef758..26e6e53f9473 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForward.qll
@@ -129,7 +129,6 @@ private class CheckUrlEncodingGuard extends Guard instanceof CheckUrlEncodingCal
/** Holds if `g` is guard for a URL that does not contain URL encoding. */
private predicate noUrlEncodingGuard(Guard g, Expr e, boolean branch) {
- g instanceof CheckUrlEncodingGuard and
e = g.(CheckUrlEncodingGuard).getCheckedExpr() and
branch = false
or
From 121b24ea7c0e21f2032afc64b54f4de85bda9275 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 27 Mar 2024 08:16:06 -0400
Subject: [PATCH 37/40] Java: remove parentheses
---
.../semmle/code/java/security/UrlForwardQuery.qll | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
index 30de4ef8354b..e90267694b53 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
@@ -12,12 +12,12 @@ module UrlForwardFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof ThreatModelFlowSource and
// excluded due to FPs
- not exists(MethodCall mc, Method m | mc.getMethod() = m |
- (
- m instanceof HttpServletRequestGetRequestUriMethod or
- m instanceof HttpServletRequestGetRequestUrlMethod or
- m instanceof HttpServletRequestGetPathMethod
- ) and
+ not exists(MethodCall mc, Method m |
+ m instanceof HttpServletRequestGetRequestUriMethod or
+ m instanceof HttpServletRequestGetRequestUrlMethod or
+ m instanceof HttpServletRequestGetPathMethod
+ |
+ mc.getMethod() = m and
mc = source.asExpr()
)
}
From 2391fe7d89ff46cad715c09963c73ae6a8fd6f90 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 27 Mar 2024 08:44:17 -0400
Subject: [PATCH 38/40] Java: use InlineFlowTest instead of
InlineExpectationsTest
---
.../security/CWE-552/UrlForwardTest.expected | 2 -
.../security/CWE-552/UrlForwardTest.java | 42 +++++++++----------
.../security/CWE-552/UrlForwardTest.ql | 18 +-------
3 files changed, 23 insertions(+), 39 deletions(-)
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.expected b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.expected
index 8ec8033d086e..e69de29bb2d1 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.expected
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.expected
@@ -1,2 +0,0 @@
-testFailures
-failures
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
index f0e982c74003..a1437a692a2f 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java
@@ -26,25 +26,25 @@ public class UrlForwardTest extends HttpServlet implements Filter {
// Spring `ModelAndView` test cases
@GetMapping("/bad1")
public ModelAndView bad1(String url) {
- return new ModelAndView(url); // $ hasUrlForward
+ return new ModelAndView(url); // $ hasTaintFlow
}
@GetMapping("/bad2")
public ModelAndView bad2(String url) {
ModelAndView modelAndView = new ModelAndView();
- modelAndView.setViewName(url); // $ hasUrlForward
+ modelAndView.setViewName(url); // $ hasTaintFlow
return modelAndView;
}
// Spring `"forward:"` prefix test cases
@GetMapping("/bad3")
public String bad3(String url) {
- return "forward:" + url + "/swagger-ui/index.html"; // $ hasUrlForward
+ return "forward:" + url + "/swagger-ui/index.html"; // $ hasTaintFlow
}
@GetMapping("/bad4")
public ModelAndView bad4(String url) {
- ModelAndView modelAndView = new ModelAndView("forward:" + url); // $ hasUrlForward
+ ModelAndView modelAndView = new ModelAndView("forward:" + url); // $ hasTaintFlow
return modelAndView;
}
@@ -60,7 +60,7 @@ public ModelAndView redirect(String url) {
@GetMapping("/bad5")
public void bad5(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher(url).include(request, response); // $ hasUrlForward
+ request.getRequestDispatcher(url).include(request, response); // $ hasTaintFlow
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
@@ -71,7 +71,7 @@ public void bad5(String url, HttpServletRequest request, HttpServletResponse res
@GetMapping("/bad6")
public void bad6(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").include(request, response); // $ hasUrlForward
+ request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").include(request, response); // $ hasTaintFlow
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
@@ -82,7 +82,7 @@ public void bad6(String url, HttpServletRequest request, HttpServletResponse res
@GetMapping("/bad7")
public void bad7(String url, HttpServletRequest request, HttpServletResponse response) {
try {
- request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").forward(request, response); // $ hasUrlForward
+ request.getRequestDispatcher("/WEB-INF/jsp/" + url + ".jsp").forward(request, response); // $ hasTaintFlow
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
@@ -106,7 +106,7 @@ public void good1(String url, HttpServletRequest request, HttpServletResponse re
public void bad8(String urlPath, HttpServletRequest request, HttpServletResponse response) {
try {
String url = "/pages" + urlPath;
- request.getRequestDispatcher(url).forward(request, response); // $ hasUrlForward
+ request.getRequestDispatcher(url).forward(request, response); // $ hasTaintFlow
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
@@ -145,7 +145,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
String path = ((HttpServletRequest) request).getServletPath();
// A sample payload "/%57EB-INF/web.xml" can bypass this `startsWith` check
if (path != null && !path.startsWith("/WEB-INF")) {
- request.getRequestDispatcher(path).forward(request, response); // $ hasUrlForward
+ request.getRequestDispatcher(path).forward(request, response); // $ hasTaintFlow
} else {
chain.doFilter(request, response);
}
@@ -158,7 +158,7 @@ public void doFilter2(ServletRequest request, ServletResponse response, FilterCh
String path = ((HttpServletRequest) request).getServletPath();
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
- request.getRequestDispatcher(path).forward(request, response); // $ hasUrlForward
+ request.getRequestDispatcher(path).forward(request, response); // $ hasTaintFlow
} else {
chain.doFilter(request, response);
}
@@ -190,7 +190,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
rd.forward(request, response);
} else {
ServletContext sc = cfg.getServletContext();
- RequestDispatcher rd = sc.getRequestDispatcher(returnURL); // $ hasUrlForward
+ RequestDispatcher rd = sc.getRequestDispatcher(returnURL); // $ hasTaintFlow
rd.forward(request, response);
}
}
@@ -206,7 +206,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
RequestDispatcher rd = request.getRequestDispatcher("/Login.jsp");
rd.forward(request, response);
} else {
- RequestDispatcher rd = request.getRequestDispatcher(returnURL); // $ hasUrlForward
+ RequestDispatcher rd = request.getRequestDispatcher(returnURL); // $ hasTaintFlow
rd.forward(request, response);
}
}
@@ -233,7 +233,7 @@ protected void doHead1(HttpServletRequest request, HttpServletResponse response)
// A sample payload "/pages/welcome.jsp/../WEB-INF/web.xml" can bypass the `startsWith` check
if (path.startsWith(BASE_PATH)) {
- request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasTaintFlow
}
}
@@ -244,7 +244,7 @@ protected void doHead2(HttpServletRequest request, HttpServletResponse response)
String path = request.getParameter("path");
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
- request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasTaintFlow
}
}
@@ -258,7 +258,7 @@ protected void doHead3(HttpServletRequest request, HttpServletResponse response)
Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
if (requestedPath.startsWith(BASE_PATH)) {
- request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ hasUrlForward
+ request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ hasTaintFlow
}
}
@@ -270,7 +270,7 @@ protected void doHead4(HttpServletRequest request, HttpServletResponse response)
Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
if (!requestedPath.startsWith("/WEB-INF") && !requestedPath.startsWith("/META-INF")) {
- request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ hasUrlForward
+ request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ hasTaintFlow
}
}
@@ -281,7 +281,7 @@ protected void doHead5(HttpServletRequest request, HttpServletResponse response)
path = URLDecoder.decode(path, "UTF-8");
if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasTaintFlow
}
}
@@ -319,7 +319,7 @@ protected void doHead8(HttpServletRequest request, HttpServletResponse response)
String path = request.getParameter("path");
if (path.contains("%")){ // incorrect check
if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasTaintFlow
}
}
}
@@ -362,14 +362,14 @@ protected void doHead11(HttpServletRequest request, HttpServletResponse response
}
if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
- request.getServletContext().getRequestDispatcher(path).include(request, response); // $ SPURIOUS: hasUrlForward
+ request.getServletContext().getRequestDispatcher(path).include(request, response); // $ SPURIOUS: hasTaintFlow
}
}
// BAD: `StaplerResponse.forward` without any checks
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object obj) throws IOException, ServletException {
String url = req.getParameter("target");
- rsp.forward(obj, url, req); // $ hasUrlForward
+ rsp.forward(obj, url, req); // $ hasTaintFlow
}
// QHelp example
@@ -381,7 +381,7 @@ protected void doGet2(HttpServletRequest request, HttpServletResponse response)
ServletContext sc = cfg.getServletContext();
// BAD: a request parameter is incorporated without validation into a URL forward
- sc.getRequestDispatcher(request.getParameter("target")).forward(request, response); // $ hasUrlForward
+ sc.getRequestDispatcher(request.getParameter("target")).forward(request, response); // $ hasTaintFlow
// GOOD: the request parameter is validated against a known fixed string
if (VALID_FORWARD.equals(request.getParameter("target"))) {
diff --git a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.ql b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.ql
index 4e62a35752bb..34841885bc34 100644
--- a/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.ql
+++ b/java/ql/test/query-tests/security/CWE-552/UrlForwardTest.ql
@@ -1,18 +1,4 @@
import java
-import TestUtilities.InlineExpectationsTest
+import TestUtilities.InlineFlowTest
import semmle.code.java.security.UrlForwardQuery
-
-module UrlForwardTest implements TestSig {
- string getARelevantTag() { result = "hasUrlForward" }
-
- predicate hasActualResult(Location location, string element, string tag, string value) {
- tag = "hasUrlForward" and
- exists(UrlForwardFlow::PathNode sink | UrlForwardFlow::flowPath(_, sink) |
- location = sink.getNode().getLocation() and
- element = sink.getNode().toString() and
- value = ""
- )
- }
-}
-
-import MakeTest
+import TaintFlowTest
From 40c932a5f911c7ad5c1ca0cd5d517ee6081f4107 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 27 Mar 2024 10:12:28 -0400
Subject: [PATCH 39/40] Java: move UrlForward.qll code to UrlForwardQuery.qll
---
.../semmle/code/java/security/UrlForward.qll | 174 -----------------
.../code/java/security/UrlForwardQuery.qll | 176 +++++++++++++++++-
2 files changed, 172 insertions(+), 178 deletions(-)
delete mode 100644 java/ql/lib/semmle/code/java/security/UrlForward.qll
diff --git a/java/ql/lib/semmle/code/java/security/UrlForward.qll b/java/ql/lib/semmle/code/java/security/UrlForward.qll
deleted file mode 100644
index 26e6e53f9473..000000000000
--- a/java/ql/lib/semmle/code/java/security/UrlForward.qll
+++ /dev/null
@@ -1,174 +0,0 @@
-/** Provides classes to reason about URL forward attacks. */
-
-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::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())
- )
- }
-}
diff --git a/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
index e90267694b53..2ca38d695512 100644
--- a/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll
@@ -1,9 +1,177 @@
-/** Provides a taint-tracking configuration for reasoning about URL forwarding. */
+/** Provides classes and a taint-tracking configuration to reason about unsafe URL forwarding. */
import java
-import semmle.code.java.security.UrlForward
-import semmle.code.java.dataflow.FlowSources
-import semmle.code.java.security.PathSanitizer
+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::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.
From 2f8c4df309a177c46aa5d2ad60919ebad815db62 Mon Sep 17 00:00:00 2001
From: Jami <57204504+jcogs33@users.noreply.github.com>
Date: Thu, 28 Mar 2024 16:15:05 -0400
Subject: [PATCH 40/40] docs wording updates
Co-authored-by: Ben Ahmady <32935794+subatoi@users.noreply.github.com>
---
java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp | 6 +++---
java/ql/src/Security/CWE/CWE-552/UrlForward.ql | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp b/java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp
index 2b06a851a2b6..71316385335c 100644
--- a/java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp
+++ b/java/ql/src/Security/CWE/CWE-552/UrlForward.qhelp
@@ -11,9 +11,9 @@ can cause file information disclosure by allowing an attacker to access unauthor
-To guard against untrusted URL forwarding, it is advisable to avoid putting user input
-directly into a forwarded URL. Instead, maintain a list of authorized
-URLs on the server; then choose from that list based on the user input provided.
+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.
diff --git a/java/ql/src/Security/CWE/CWE-552/UrlForward.ql b/java/ql/src/Security/CWE/CWE-552/UrlForward.ql
index 95c540049a21..91e244a81522 100644
--- a/java/ql/src/Security/CWE/CWE-552/UrlForward.ql
+++ b/java/ql/src/Security/CWE/CWE-552/UrlForward.ql
@@ -1,6 +1,6 @@
/**
* @name URL forward from a remote source
- * @description URL forward based on unvalidated user-input
+ * @description URL forward based on unvalidated user input
* may cause file information disclosure.
* @kind path-problem
* @problem.severity error