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