diff --git a/java/ql/src/experimental/Security/CWE/CWE-073/FilePathInjection.java b/java/ql/src/experimental/Security/CWE/CWE-073/FilePathInjection.java new file mode 100644 index 000000000000..6bee08e2f3ac --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-073/FilePathInjection.java @@ -0,0 +1,21 @@ +// BAD: no file download validation +HttpServletRequest request = getRequest(); +String path = request.getParameter("path"); +String filePath = "/pages/" + path; +HttpServletResponse resp = getResponse(); +File file = new File(filePath); +resp.getOutputStream().write(file.readContent()); + +// BAD: no file upload validation +String savePath = getPara("dir"); +File file = getFile("fileParam").getFile(); +FileInputStream fis = new FileInputStream(file); +String filePath = "/files/" + savePath; +FileOutputStream fos = new FileOutputStream(filePath); + +// 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 (!filePath.contains("..") && filePath.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 (filePath.hasPrefix("/files") && !filePath.contains("%")) { ... } \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-073/FilePathInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-073/FilePathInjection.qhelp new file mode 100644 index 000000000000..0797fb366ffc --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-073/FilePathInjection.qhelp @@ -0,0 +1,39 @@ + + + + + +

External Control of File Name or Path, also called File Path Injection, is a vulnerability by which +a file path is created using data from outside the application (such as the HTTP request). It allows +an attacker to traverse through the filesystem and access arbitrary files.

+
+ + +

Unsanitized user-provided data must not be used to construct file paths. In order to prevent File +Path Injection, it is recommended to avoid concatenating user input directly into the file path. Instead, +user input should be checked against allowed or disallowed paths (for example, the path must be within +/user_content/ or must not be within /internal), ensuring that neither path +traversal using ../ nor URL encoding is 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 to construct a file path +without validating the input, which may cause file leakage. In the GOOD method, the file path +is validated. +

+ +
+ + +
  • OWASP: + Path Traversal. +
  • +
  • Veracode: + External Control of File Name or Path Flaw. +
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-073/FilePathInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-073/FilePathInjection.ql new file mode 100644 index 000000000000..bf4f1ec33bb9 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-073/FilePathInjection.ql @@ -0,0 +1,43 @@ +/** + * @name File Path Injection + * @description Loading files based on unvalidated user-input may cause file information disclosure + * and uploading files with unvalidated file types to an arbitrary directory may lead to + * Remote Command Execution (RCE). + * @kind path-problem + * @problem.severity error + * @precision high + * @id java/file-path-injection + * @tags security + * external/cwe-073 + */ + +import java +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.security.PathCreation +import JFinalController +import experimental.semmle.code.java.PathSanitizer +import DataFlow::PathGraph + +class InjectFilePathConfig extends TaintTracking::Configuration { + InjectFilePathConfig() { this = "InjectFilePathConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + sink.asExpr() = any(PathCreation p).getAnInput() and + not sink instanceof NormalizedPathNode + } + + override predicate isSanitizer(DataFlow::Node node) { + exists(Type t | t = node.getType() | t instanceof BoxedType or t instanceof PrimitiveType) + } + + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { + guard instanceof PathTraversalBarrierGuard + } +} + +from DataFlow::PathNode source, DataFlow::PathNode sink, InjectFilePathConfig conf +where conf.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "External control of file name or path due to $@.", + source.getNode(), "user-provided value" diff --git a/java/ql/src/experimental/Security/CWE/CWE-073/JFinalController.qll b/java/ql/src/experimental/Security/CWE/CWE-073/JFinalController.qll new file mode 100644 index 000000000000..264e1e31ed26 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-073/JFinalController.qll @@ -0,0 +1,83 @@ +import java +import semmle.code.java.dataflow.FlowSources + +/** The class `com.jfinal.core.Controller`. */ +class JFinalController extends RefType { + JFinalController() { this.hasQualifiedName("com.jfinal.core", "Controller") } +} + +/** The method `getSessionAttr` of `JFinalController`. */ +class GetSessionAttributeMethod extends Method { + GetSessionAttributeMethod() { + this.getName() = "getSessionAttr" and + this.getDeclaringType().getASupertype*() instanceof JFinalController + } +} + +/** The method `setSessionAttr` of `JFinalController`. */ +class SetSessionAttributeMethod extends Method { + SetSessionAttributeMethod() { + this.getName() = "setSessionAttr" and + this.getDeclaringType().getASupertype*() instanceof JFinalController + } +} + +/** A request attribute getter method of `JFinalController`. */ +class GetRequestAttributeMethod extends Method { + GetRequestAttributeMethod() { + this.getName().matches("getAttr%") and + this.getDeclaringType().getASupertype*() instanceof JFinalController + } +} + +/** A request attribute setter method of `JFinalController`. */ +class SetRequestAttributeMethod extends Method { + SetRequestAttributeMethod() { + this.getName() = ["set", "setAttr"] and + this.getDeclaringType().getASupertype*() instanceof JFinalController + } +} + +/** + * Value step from a setter call to a corresponding getter call relating to a + * session or request attribute. + */ +private class SetToGetAttributeStep extends AdditionalValueStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + exists(MethodAccess gma, MethodAccess sma | + ( + gma.getMethod() instanceof GetSessionAttributeMethod and + sma.getMethod() instanceof SetSessionAttributeMethod + or + gma.getMethod() instanceof GetRequestAttributeMethod and + sma.getMethod() instanceof SetRequestAttributeMethod + ) and + gma.getArgument(0).(CompileTimeConstantExpr).getStringValue() = + sma.getArgument(0).(CompileTimeConstantExpr).getStringValue() + | + pred.asExpr() = sma.getArgument(1) and + succ.asExpr() = gma + ) + } +} + +/** Remote flow source models relating to `JFinal`. */ +private class JFinalControllerSource extends SourceModelCsv { + override predicate row(string row) { + row = + [ + "com.jfinal.core;Controller;true;getCookie" + ["", "Object", "Objects", "ToInt", "ToLong"] + + ";;;ReturnValue;remote", + "com.jfinal.core;Controller;true;getFile" + ["", "s"] + ";;;ReturnValue;remote", + "com.jfinal.core;Controller;true;getHeader;;;ReturnValue;remote", + "com.jfinal.core;Controller;true;getKv;;;ReturnValue;remote", + "com.jfinal.core;Controller;true;getPara" + + [ + "", "Map", "ToBoolean", "ToDate", "ToInt", "ToLong", "Values", "ValuesToInt", + "ValuesToLong" + ] + ";;;ReturnValue;remote", + "com.jfinal.core;Controller;true;get" + ["", "Int", "Long", "Boolean", "Date"] + + ";;;ReturnValue;remote" + ] + } +} diff --git a/java/ql/src/experimental/semmle/code/java/PathSanitizer.qll b/java/ql/src/experimental/semmle/code/java/PathSanitizer.qll new file mode 100644 index 000000000000..9e76410a6ffb --- /dev/null +++ b/java/ql/src/experimental/semmle/code/java/PathSanitizer.qll @@ -0,0 +1,193 @@ +import java +import semmle.code.java.controlflow.Guards +import semmle.code.java.dataflow.FlowSources + +/** A barrier guard that protects against path traversal vulnerabilities. */ +abstract class PathTraversalBarrierGuard extends DataFlow::BarrierGuard { } + +/** + * A guard that considers safe a string being exactly compared to a trusted value. + */ +private class ExactStringPathMatchGuard extends PathTraversalBarrierGuard instanceof MethodAccess { + ExactStringPathMatchGuard() { + super.getMethod().getDeclaringType() instanceof TypeString and + super.getMethod().getName() = ["equals", "equalsIgnoreCase"] + } + + override predicate checks(Expr e, boolean branch) { + e = super.getQualifier() and + branch = true + } +} + +private class AllowListGuard extends Guard instanceof MethodAccess { + AllowListGuard() { + (isStringPartialMatch(this) or isPathPartialMatch(this)) and + not isDisallowedWord(super.getAnArgument()) + } + + Expr getCheckedExpr() { result = super.getQualifier() } +} + +/** + * A guard that considers a path safe because it is checked against an allowlist of partial trusted values. + * This requires additional protection against path traversal, either another guard (`PathTraversalGuard`) + * or a sanitizer (`PathNormalizeSanitizer`), to ensure any internal `..` components are removed from the path. + */ +private class AllowListBarrierGuard extends PathTraversalBarrierGuard instanceof AllowListGuard { + override predicate checks(Expr e, boolean branch) { + e = super.getCheckedExpr() and + branch = true and + ( + // Either a path normalization sanitizer comes before the guard, + exists(PathNormalizeSanitizer sanitizer | DataFlow::localExprFlow(sanitizer, e)) + or + // or a check like `!path.contains("..")` comes before the guard + exists(PathTraversalGuard previousGuard | + DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and + previousGuard.controls(this.getBasicBlock().(ConditionBlock), false) + ) + ) + } +} + +/** + * A guard that considers a path safe because it is checked for `..` components, having previously + * been checked for a trusted prefix. + */ +private class DotDotCheckBarrierGuard extends PathTraversalBarrierGuard instanceof PathTraversalGuard { + override predicate checks(Expr e, boolean branch) { + e = super.getCheckedExpr() and + branch = false and + // The same value has previously been checked against a list of allowed prefixes: + exists(AllowListGuard previousGuard | + DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and + previousGuard.controls(this.getBasicBlock().(ConditionBlock), true) + ) + } +} + +private class BlockListGuard extends Guard instanceof MethodAccess { + BlockListGuard() { + (isStringPartialMatch(this) or isPathPartialMatch(this)) and + isDisallowedWord(super.getAnArgument()) + } + + Expr getCheckedExpr() { result = super.getQualifier() } +} + +/** + * A guard that considers a string safe because it is checked against a blocklist of known dangerous values. + * This requires a prior check for URL encoding concealing a forbidden value, either a guard (`UrlEncodingGuard`) + * or a sanitizer (`UrlDecodeSanitizer`). + */ +private class BlockListBarrierGuard extends PathTraversalBarrierGuard instanceof BlockListGuard { + override predicate checks(Expr e, boolean branch) { + e = super.getCheckedExpr() and + branch = false and + ( + // Either `e` has been URL decoded: + exists(UrlDecodeSanitizer sanitizer | DataFlow::localExprFlow(sanitizer, e)) + or + // or `e` has previously been checked for URL encoding sequences: + exists(UrlEncodingGuard previousGuard | + DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and + previousGuard.controls(this.getBasicBlock(), false) + ) + ) + } +} + +/** + * 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. + */ +private class URLEncodingBarrierGuard extends PathTraversalBarrierGuard instanceof UrlEncodingGuard { + override predicate checks(Expr e, boolean branch) { + e = super.getCheckedExpr() and + branch = false and + exists(BlockListGuard previousGuard | + DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and + previousGuard.controls(this.getBasicBlock(), false) + ) + } +} + +/** + * Holds if `ma` is a call to a method that checks a partial string match. + */ +private predicate isStringPartialMatch(MethodAccess ma) { + ma.getMethod().getDeclaringType() instanceof TypeString and + ma.getMethod().getName() = + ["contains", "startsWith", "matches", "regionMatches", "indexOf", "lastIndexOf"] +} + +/** + * Holds if `ma` is a call to a method of `java.nio.file.Path` that checks a partial path match. + */ +private predicate isPathPartialMatch(MethodAccess ma) { + ma.getMethod().getDeclaringType() instanceof TypePath and + ma.getMethod().getName() = "startsWith" +} + +private predicate isDisallowedWord(CompileTimeConstantExpr word) { + word.getStringValue().matches(["%WEB-INF%", "%META-INF%", "%..%"]) +} + +/** A complementary guard that protects against path traversal, by looking for the literal `..`. */ +class PathTraversalGuard extends Guard instanceof MethodAccess { + Expr checked; + + PathTraversalGuard() { + super.getMethod().getDeclaringType() instanceof TypeString and + super.getMethod().hasName(["contains", "indexOf"]) and + super.getAnArgument().(CompileTimeConstantExpr).getStringValue() = ".." + } + + Expr getCheckedExpr() { result = super.getQualifier() } +} + +/** A complementary sanitizer that protects against path traversal using path normalization. */ +private class PathNormalizeSanitizer extends MethodAccess { + PathNormalizeSanitizer() { + this.getMethod().getDeclaringType().hasQualifiedName("java.nio.file", "Path") and + this.getMethod().hasName("normalize") + } +} + +/** A complementary guard that protects against double URL encoding, by looking for the literal `%`. */ +private class UrlEncodingGuard extends Guard instanceof MethodAccess { + UrlEncodingGuard() { + super.getMethod().getDeclaringType() instanceof TypeString and + super.getMethod().hasName(["contains", "indexOf"]) and + super.getAnArgument().(CompileTimeConstantExpr).getStringValue() = "%" + } + + Expr getCheckedExpr() { result = super.getQualifier() } +} + +/** A complementary sanitizer that protects against double URL encoding using URL decoding. */ +private class UrlDecodeSanitizer extends MethodAccess { + UrlDecodeSanitizer() { + this.getMethod().getDeclaringType().hasQualifiedName("java.net", "URLDecoder") and + this.getMethod().hasName("decode") + } +} + +/** A node with path normalization. */ +class NormalizedPathNode extends DataFlow::Node { + NormalizedPathNode() { + TaintTracking::localExprTaint(this.asExpr(), any(PathNormalizeSanitizer ma)) + } +} + +/** Data model related to `java.nio.file.Path`. */ +private class PathDataModel extends SummaryModelCsv { + override predicate row(string row) { + row = + [ + "java.nio.file;Paths;true;get;;;Argument[0];ReturnValue;taint", + "java.nio.file;Path;true;normalize;;;Argument[-1];ReturnValue;taint" + ] + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-073/FilePathInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-073/FilePathInjection.expected new file mode 100644 index 000000000000..5720de5c4b98 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-073/FilePathInjection.expected @@ -0,0 +1,20 @@ +edges +| FilePathInjection.java:21:21:21:34 | getPara(...) : String | FilePathInjection.java:26:47:26:59 | finalFilePath | +| FilePathInjection.java:64:21:64:34 | getPara(...) : String | FilePathInjection.java:72:47:72:59 | finalFilePath | +| FilePathInjection.java:87:21:87:34 | getPara(...) : String | FilePathInjection.java:95:47:95:59 | finalFilePath | +| FilePathInjection.java:205:17:205:44 | getParameter(...) : String | FilePathInjection.java:209:24:209:31 | filePath | +nodes +| FilePathInjection.java:21:21:21:34 | getPara(...) : String | semmle.label | getPara(...) : String | +| FilePathInjection.java:26:47:26:59 | finalFilePath | semmle.label | finalFilePath | +| FilePathInjection.java:64:21:64:34 | getPara(...) : String | semmle.label | getPara(...) : String | +| FilePathInjection.java:72:47:72:59 | finalFilePath | semmle.label | finalFilePath | +| FilePathInjection.java:87:21:87:34 | getPara(...) : String | semmle.label | getPara(...) : String | +| FilePathInjection.java:95:47:95:59 | finalFilePath | semmle.label | finalFilePath | +| FilePathInjection.java:205:17:205:44 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| FilePathInjection.java:209:24:209:31 | filePath | semmle.label | filePath | +subpaths +#select +| FilePathInjection.java:26:47:26:59 | finalFilePath | FilePathInjection.java:21:21:21:34 | getPara(...) : String | FilePathInjection.java:26:47:26:59 | finalFilePath | External control of file name or path due to $@. | FilePathInjection.java:21:21:21:34 | getPara(...) | user-provided value | +| FilePathInjection.java:72:47:72:59 | finalFilePath | FilePathInjection.java:64:21:64:34 | getPara(...) : String | FilePathInjection.java:72:47:72:59 | finalFilePath | External control of file name or path due to $@. | FilePathInjection.java:64:21:64:34 | getPara(...) | user-provided value | +| FilePathInjection.java:95:47:95:59 | finalFilePath | FilePathInjection.java:87:21:87:34 | getPara(...) : String | FilePathInjection.java:95:47:95:59 | finalFilePath | External control of file name or path due to $@. | FilePathInjection.java:87:21:87:34 | getPara(...) | user-provided value | +| FilePathInjection.java:209:24:209:31 | filePath | FilePathInjection.java:205:17:205:44 | getParameter(...) : String | FilePathInjection.java:209:24:209:31 | filePath | External control of file name or path due to $@. | FilePathInjection.java:205:17:205:44 | getParameter(...) | user-provided value | diff --git a/java/ql/test/experimental/query-tests/security/CWE-073/FilePathInjection.java b/java/ql/test/experimental/query-tests/security/CWE-073/FilePathInjection.java new file mode 100644 index 000000000000..2534386a2106 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-073/FilePathInjection.java @@ -0,0 +1,245 @@ +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import com.jfinal.core.Controller; + +public class FilePathInjection extends Controller { + private static final String BASE_PATH = "/pages"; + + // BAD: Upload file to user specified path without validation + public void uploadFile() throws IOException { + String savePath = getPara("dir"); + File file = getFile("fileParam").getFile(); + String finalFilePath = BASE_PATH + savePath; + + FileInputStream fis = new FileInputStream(file); + FileOutputStream fos = new FileOutputStream(finalFilePath); + int i = 0; + + do { + byte[] buf = new byte[1024]; + i = fis.read(buf); + fos.write(buf); + } while (i != -1); + + fis.close(); + fos.close(); + } + + // GOOD: Upload file to user specified path with path normalization and validation + public void uploadFile2() throws IOException { + String savePath = getPara("dir"); + File file = getFile("fileParam").getFile(); + String finalFilePath = BASE_PATH + savePath; + Path path = Paths.get(finalFilePath).normalize(); + + if (path.startsWith(BASE_PATH)) { + FileInputStream fis = new FileInputStream(file); + FileOutputStream fos = new FileOutputStream(path.toFile()); + int i = 0; + + do { + byte[] buf = new byte[1024]; + i = fis.read(buf); + fos.write(buf); + } while (i != -1); + + fis.close(); + fos.close(); + } + } + + // BAD: Upload file to user specified path without validation through session attribute + public void uploadFile3() throws IOException { + String savePath = getPara("dir"); + setSessionAttr("uploadDir", savePath); + String sessionUploadDir = getSessionAttr("uploadDir"); + + File file = getFile("fileParam").getFile(); + String finalFilePath = BASE_PATH + sessionUploadDir; + + FileInputStream fis = new FileInputStream(file); + FileOutputStream fos = new FileOutputStream(finalFilePath); + int i = 0; + + do { + byte[] buf = new byte[1024]; + i = fis.read(buf); + fos.write(buf); + } while (i != -1); + + fis.close(); + fos.close(); + } + + // BAD: Upload file to user specified path without validation through request attribute + public void uploadFile4() throws IOException { + String savePath = getPara("dir"); + setAttr("uploadDir2", savePath); + String requestUploadDir = getAttr("uploadDir2"); + + File file = getFile("fileParam").getFile(); + String finalFilePath = BASE_PATH + requestUploadDir; + + FileInputStream fis = new FileInputStream(file); + FileOutputStream fos = new FileOutputStream(finalFilePath); + int i = 0; + + do { + byte[] buf = new byte[1024]; + i = fis.read(buf); + fos.write(buf); + } while (i != -1); + + fis.close(); + fos.close(); + } + + // BAD: Upload file to user specified path without validation through session object (not detected) + public void uploadFile5() throws IOException { + String savePath = getPara("dir"); + getSession().setAttribute("uploadDir3", savePath); + String sessionUploadDir = getSessionAttr("uploadDir3"); + + File file = getFile("fileParam").getFile(); + String finalFilePath = BASE_PATH + sessionUploadDir; + + FileInputStream fis = new FileInputStream(file); + FileOutputStream fos = new FileOutputStream(finalFilePath); + int i = 0; + + do { + byte[] buf = new byte[1024]; + i = fis.read(buf); + fos.write(buf); + } while (i != -1); + + fis.close(); + fos.close(); + } + + // GOOD: Upload file to a system path from a request object + public void uploadFile6() throws IOException { + setAttr("uploadDir4", "/data/upload_dir/"); + String requestUploadDir = getAttr("uploadDir4"); + + File file = getFile("fileParam").getFile(); + String finalFilePath = BASE_PATH + requestUploadDir; + + FileInputStream fis = new FileInputStream(file); + FileOutputStream fos = new FileOutputStream(finalFilePath); + int i = 0; + + do { + byte[] buf = new byte[1024]; + i = fis.read(buf); + fos.write(buf); + } while (i != -1); + + fis.close(); + fos.close(); + } + + // GOOD: Upload file to a system path from a request object + public void uploadFile7() throws IOException { + String savePath = getPara("dir"); + setAttr("uploadDir5", savePath); + setAttr("realUploadDir", "/data/upload_dir/"); + String requestUploadDir = getAttr("realUploadDir5"); + + File file = getFile("fileParam").getFile(); + String finalFilePath = BASE_PATH + requestUploadDir; + + FileInputStream fis = new FileInputStream(file); + FileOutputStream fos = new FileOutputStream(finalFilePath); + int i = 0; + + do { + byte[] buf = new byte[1024]; + i = fis.read(buf); + fos.write(buf); + } while (i != -1); + + fis.close(); + fos.close(); + } + + private void readFile(HttpServletResponse resp, File file) { + OutputStream os = null; + FileInputStream fis = null; + try { + os = resp.getOutputStream(); + fis = new FileInputStream(file); + byte fileContent[] = new byte[(int) file.length()]; + fis.read(fileContent); + os.write(fileContent); + } catch (Exception e) { + System.err.println("Invalid directory or file " + file.getName()); + } finally { + try { + if (os != null) + os.close(); + } catch (Exception e2) { + } + try { + if (fis != null) + fis.close(); + } catch (Exception e2) { + } + } + } + + // BAD: Download file to user specified path without validation + public void downloadFile() throws FileNotFoundException, IOException { + HttpServletRequest request = getRequest(); + String path = request.getParameter("path"); + String filePath = BASE_PATH + path; + + HttpServletResponse resp = getResponse(); + File file = new File(filePath); + if (path != null && file.exists()) { + resp.setHeader("Content-type", "application/force-download"); + resp.setHeader("Content-Disposition", "inline;filename=\"" + filePath + "\""); + resp.setHeader("Content-Transfer-Encoding", "Binary"); + resp.setHeader("Content-length", "" + file.length()); + resp.setHeader("Content-Type", "application/octet-stream"); + resp.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\""); + readFile(resp, file); + } else { + System.err.println("File does not exist " + path); + } + } + + // GOOD: Download file with path validation + public void downloadFile2() throws FileNotFoundException, IOException { + HttpServletRequest request = getRequest(); + String path = request.getParameter("path"); + String filePath = BASE_PATH + path; + + HttpServletResponse resp = getResponse(); + if (!filePath.contains("..") && filePath.startsWith(BASE_PATH)) { + File file = new File(filePath); + if (file.exists()) { + resp.setHeader("Content-type", "application/force-download"); + resp.setHeader("Content-Disposition", "inline;filename=\"" + filePath + "\""); + resp.setHeader("Content-Transfer-Encoding", "Binary"); + resp.setHeader("Content-length", "" + file.length()); + resp.setHeader("Content-Type", "application/octet-stream"); + resp.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\""); + readFile(resp, file); + } else { + System.err.println("File does not exist " + path); + } + } + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-073/FilePathInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-073/FilePathInjection.qlref new file mode 100644 index 000000000000..3c6db8058a65 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-073/FilePathInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-073/FilePathInjection.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-073/options b/java/ql/test/experimental/query-tests/security/CWE-073/options new file mode 100644 index 000000000000..373894974154 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-073/options @@ -0,0 +1 @@ +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jfinal-4.9.15 \ No newline at end of file diff --git a/java/ql/test/stubs/jfinal-4.9.15/com/jfinal/core/Controller.java b/java/ql/test/stubs/jfinal-4.9.15/com/jfinal/core/Controller.java new file mode 100644 index 000000000000..72884b70268b --- /dev/null +++ b/java/ql/test/stubs/jfinal-4.9.15/com/jfinal/core/Controller.java @@ -0,0 +1,347 @@ +package com.jfinal.core; + +import java.io.File; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import com.jfinal.kit.Kv; +import com.jfinal.upload.UploadFile; + +public abstract class Controller { + public String getPara(String name) { + return null; + } + + public String getPara(String name, String defaultValue) { + return null; + } + + public Map getParaMap() { + return null; + } + + public Enumeration getParaNames() { + return null; + } + + public String[] getParaValues(String name) { + return null; + } + + public Integer[] getParaValuesToInt(String name) { + return null; + } + + public Long[] getParaValuesToLong(String name) { + return null; + } + + public Controller setAttr(String name, Object value) { + return null; + } + + public Controller setAttrs(Map attrMap) { + return null; + } + + public Enumeration getAttrNames() { + return null; + } + + public T getAttr(String name) { + return null; + } + + public T getAttr(String name, T defaultValue) { + return null; + } + + public String getAttrForStr(String name) { + return null; + } + + public Integer getAttrForInt(String name) { + return -1; + } + + public String getHeader(String name) { + return null; + } + + public Integer getParaToInt(String name) { + return null; + } + + public Integer getParaToInt(String name, Integer defaultValue) { + return null; + } + + public Long getParaToLong(String name) { + return null; + } + + public Long getParaToLong(String name, Long defaultValue) { + return null; + } + + public Boolean getParaToBoolean(String name) { + return false; + } + + public Boolean getParaToBoolean(String name, Boolean defaultValue) { + return false; + } + + public Boolean getParaToBoolean() { + return false; + } + + public Boolean getParaToBoolean(int index) { + return false; + } + + public Boolean getParaToBoolean(int index, Boolean defaultValue) { + return false; + } + + public Date getParaToDate(String name) { + return null; + } + + public Date getParaToDate(String name, Date defaultValue) { + return null; + } + + public Date getParaToDate() { + return null; + } + + public HttpServletRequest getRequest() { + return null; + } + + public HttpServletResponse getResponse() { + return null; + } + + public HttpSession getSession() { + return null; + } + + public HttpSession getSession(boolean create) { + return null; + } + + public T getSessionAttr(String key) { + return null; + } + + public T getSessionAttr(String key, T defaultValue) { + return null; + } + + public Controller setSessionAttr(String key, Object value) { + return null; + } + + public String getCookie(String name, String defaultValue) { + return null; + } + + public String getCookie(String name) { + return null; + } + + public Integer getCookieToInt(String name) { + return null; + } + + public Integer getCookieToInt(String name, Integer defaultValue) { + return null; + } + + public Long getCookieToLong(String name) { + return null; + } + + public Long getCookieToLong(String name, Long defaultValue) { + return null; + } + + public Cookie getCookieObject(String name) { + return null; + } + + public Cookie[] getCookieObjects() { + return null; + } + + public String getPara() { + return null; + } + + public String getPara(int index) { + return null; + } + + public String getPara(int index, String defaultValue) { + return null; + } + + public Integer getParaToInt(int index) { + return null; + } + + public Integer getParaToInt(int index, Integer defaultValue) { + return null; + } + + public Long getParaToLong(int index) { + return null; + } + + public Long getParaToLong(int index, Long defaultValue) { + return null; + } + + public Integer getParaToInt() { + return null; + } + + public Long getParaToLong() { + return null; + } + + public Kv getKv() { + return null; + } + + public List getFiles(String uploadPath, Integer maxPostSize, String encoding) { + return null; + } + + public UploadFile getFile(String parameterName, String uploadPath, Integer maxPostSize, String encoding) { + return null; + } + + public List getFiles(String uploadPath, int maxPostSize) { + return null; + } + + public UploadFile getFile(String parameterName, String uploadPath, int maxPostSize) { + return null; + } + + public List getFiles(String uploadPath) { + return null; + } + + public UploadFile getFile(String parameterName, String uploadPath) { + return null; + } + + public List getFiles() { + return null; + } + + public UploadFile getFile() { + return null; + } + + public UploadFile getFile(String parameterName) { + return null; + } + + public Controller set(String attributeName, Object attributeValue) { + return null; + } + + public String get(String name) { + return null; + } + + public String get(String name, String defaultValue) { + return null; + } + + public Integer getInt(String name) { + return -1; + } + + public Integer getInt(String name, Integer defaultValue) { + return -1; + } + + public Long getLong(String name) { + return null; + } + + public Long getLong(String name, Long defaultValue) { + return null; + } + + public Boolean getBoolean(String name) { + return false; + } + + public Boolean getBoolean(String name, Boolean defaultValue) { + return false; + } + + public Date getDate(String name) { + return null; + } + + public Date getDate(String name, Date defaultValue) { + return null; + } + + public String get(int index) { + return null; + } + + public String get(int index, String defaultValue) { + return null; + } + + public Integer getInt() { + return -1; + } + + public Integer getInt(int index) { + return -1; + } + + public Integer getInt(int index, Integer defaultValue) { + return -1; + } + + public Long getLong() { + return null; + } + + public Long getLong(int index) { + return null; + } + + public Long getLong(int index, Long defaultValue) { + return null; + } + + public Boolean getBoolean() { + return false; + } + + public Boolean getBoolean(int index) { + return false; + } + + public Boolean getBoolean(int index, Boolean defaultValue) { + return false; + } +} diff --git a/java/ql/test/stubs/jfinal-4.9.15/com/jfinal/kit/Kv.java b/java/ql/test/stubs/jfinal-4.9.15/com/jfinal/kit/Kv.java new file mode 100644 index 000000000000..ff8b112d05c9 --- /dev/null +++ b/java/ql/test/stubs/jfinal-4.9.15/com/jfinal/kit/Kv.java @@ -0,0 +1,6 @@ +package com.jfinal.kit; + +import java.util.HashMap; + +public class Kv extends HashMap { +} diff --git a/java/ql/test/stubs/jfinal-4.9.15/com/jfinal/upload/UploadFile.java b/java/ql/test/stubs/jfinal-4.9.15/com/jfinal/upload/UploadFile.java new file mode 100644 index 000000000000..2e6063c48529 --- /dev/null +++ b/java/ql/test/stubs/jfinal-4.9.15/com/jfinal/upload/UploadFile.java @@ -0,0 +1,32 @@ +package com.jfinal.upload; + +import java.io.File; + +public class UploadFile { + public UploadFile(String parameterName, String uploadPath, String filesystemName, String originalFileName, String contentType) { + } + + public String getParameterName() { + return null; + } + + public String getFileName() { + return null; + } + + public String getOriginalFileName() { + return null; + } + + public String getContentType() { + return null; + } + + public String getUploadPath() { + return null; + } + + public File getFile() { + return null; + } +}