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 - +

    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