From 9ff4ed286f7a060fa77475c6e846546336e0e279 Mon Sep 17 00:00:00 2001 From: MarkLee131 Date: Sat, 4 Apr 2026 20:57:03 +0800 Subject: [PATCH] Java: recognize Path.toRealPath() as path normalization sanitizer PathNormalizeSanitizer recognized Path.normalize() and File.getCanonicalPath()/getCanonicalFile(), but not Path.toRealPath(). toRealPath() is strictly stronger than normalize() (resolves symlinks and verifies file existence in addition to normalizing ".." components), and is functionally equivalent to File.getCanonicalPath() for the NIO.2 API. CERT FIO16-J and OWASP both recommend it for path traversal defense. This adds toRealPath to PathNormalizeSanitizer alongside normalize, reducing false positives for code using idiomatic NIO.2 path handling. --- .../2026-04-04-path-injection-torealpath.md | 4 ++++ .../code/java/security/PathSanitizer.qll | 2 +- .../CWE-022/semmle/tests/TaintedPath.java | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 java/ql/lib/change-notes/2026-04-04-path-injection-torealpath.md diff --git a/java/ql/lib/change-notes/2026-04-04-path-injection-torealpath.md b/java/ql/lib/change-notes/2026-04-04-path-injection-torealpath.md new file mode 100644 index 00000000000..8856d419bce --- /dev/null +++ b/java/ql/lib/change-notes/2026-04-04-path-injection-torealpath.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The `java/path-injection` and `java/zipslip` queries now recognize `Path.toRealPath()` as a path normalization sanitizer, consistent with the existing treatment of `Path.normalize()` and `File.getCanonicalPath()`. This reduces false positives for code that uses the NIO.2 API for path canonicalization. diff --git a/java/ql/lib/semmle/code/java/security/PathSanitizer.qll b/java/ql/lib/semmle/code/java/security/PathSanitizer.qll index 788cd542939..e2957f6b02f 100644 --- a/java/ql/lib/semmle/code/java/security/PathSanitizer.qll +++ b/java/ql/lib/semmle/code/java/security/PathSanitizer.qll @@ -243,7 +243,7 @@ private class PathNormalizeSanitizer extends MethodCall { PathNormalizeSanitizer() { exists(RefType t | this.getMethod().getDeclaringType() = t | (t instanceof TypePath or t instanceof FilesKt) and - this.getMethod().hasName("normalize") + this.getMethod().hasName(["normalize", "toRealPath"]) or t instanceof TypeFile and this.getMethod().hasName(["getCanonicalPath", "getCanonicalFile"]) diff --git a/java/ql/test/query-tests/security/CWE-022/semmle/tests/TaintedPath.java b/java/ql/test/query-tests/security/CWE-022/semmle/tests/TaintedPath.java index 442873b54a4..fb87c687823 100644 --- a/java/ql/test/query-tests/security/CWE-022/semmle/tests/TaintedPath.java +++ b/java/ql/test/query-tests/security/CWE-022/semmle/tests/TaintedPath.java @@ -72,6 +72,27 @@ public class TaintedPath { } } + public void sendUserFileGood5(Socket sock, String user) throws Exception { + BufferedReader filenameReader = + new BufferedReader(new InputStreamReader(sock.getInputStream(), "UTF-8")); + String filename = filenameReader.readLine(); + + Path publicFolder = Paths.get("/home/" + user + "/public").toRealPath(); + Path filePath = publicFolder.resolve(filename).toRealPath(); + + // GOOD: toRealPath() normalizes the path (resolves ".." and symlinks), + // equivalent to File.getCanonicalPath() + if (!filePath.startsWith(publicFolder + File.separator)) { + throw new IllegalArgumentException("Invalid filename"); + } + BufferedReader fileReader = new BufferedReader(new FileReader(filePath.toString())); + String fileLine = fileReader.readLine(); + while (fileLine != null) { + sock.getOutputStream().write(fileLine.getBytes()); + fileLine = fileReader.readLine(); + } + } + public void sendUserFileGood4(Socket sock, String user) throws IOException { BufferedReader filenameReader = new BufferedReader(new InputStreamReader(sock.getInputStream(), "UTF-8"));