Skip to content

Commit 661c5cf

Browse files
authored
Merge pull request #15409 from erik-krogh/path-java
Java: Improve the QHelp for `java/path-injection`.
2 parents b2489f7 + 282632c commit 661c5cf

File tree

10 files changed

+125
-47
lines changed

10 files changed

+125
-47
lines changed

java/ql/src/Security/CWE/CWE-022/TaintedPath.qhelp

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,28 @@
77
can result in sensitive information being revealed or deleted, or an attacker being able to influence
88
behavior by modifying unexpected files.</p>
99

10-
<p>Paths that are naively constructed from data controlled by a user may contain unexpected special characters,
11-
such as "..". Such a path may potentially point anywhere on the file system.</p>
10+
<p>Paths that are naively constructed from data controlled by a user may be absolute paths, or may contain
11+
unexpected special characters such as "..". Such a path could point anywhere on the file system.</p>
1212

1313
</overview>
1414
<recommendation>
1515

1616
<p>Validate user input before using it to construct a file path.</p>
1717

18-
<p>The choice of validation depends on whether you want to allow the user to specify complex paths with
19-
multiple components that may span multiple folders, or only simple filenames without a path component.</p>
18+
<p>Common validation methods include checking that the normalized path is relative and does not contain
19+
any ".." components, or checking that the path is contained within a safe folder. The method you should use depends
20+
on how the path is used in the application, and whether the path should be a single path component.
21+
</p>
2022

21-
<p>In the former case, a common strategy is to make sure that the constructed file path is contained within
22-
a safe root folder, for example by checking that the path starts with the root folder. Additionally,
23-
you need to ensure that the path does not contain any ".." components, since otherwise
24-
even a path that starts with the root folder could be used to access files outside the root folder.</p>
23+
<p>If the path should be a single path component (such as a file name), you can check for the existence
24+
of any path separators ("/" or "\"), or ".." sequences in the input, and reject the input if any are found.
25+
</p>
2526

26-
<p>In the latter case, if you want to ensure that the user input is interpreted as a simple filename without
27-
a path component, you can remove all path separators ("/" or "\") and all ".." sequences from the input
28-
before using it to construct a file path. Note that it is <i>not</i> sufficient to only remove "../" sequences:
29-
for example, applying this filter to ".../...//" would still result in the string "../".</p>
27+
<p>
28+
Note that removing "../" sequences is <i>not</i> sufficient, since the input could still contain a path separator
29+
followed by "..". For example, the input ".../...//" would still result in the string "../" if only "../" sequences
30+
are removed.
31+
</p>
3032

3133
<p>Finally, the simplest (but most restrictive) option is to use an allow list of safe patterns and make sure that
3234
the user input matches one of these patterns.</p>
@@ -36,15 +38,22 @@ the user input matches one of these patterns.</p>
3638

3739
<p>In this example, a file name is read from a <code>java.net.Socket</code> and then used to access a file
3840
and send it back over the socket. However, a malicious user could enter a file name anywhere on the file system,
39-
such as "/etc/passwd".</p>
41+
such as "/etc/passwd" or "../../../etc/passwd".</p>
4042

41-
<sample src="TaintedPath.java" />
43+
<sample src="examples/TaintedPath.java" />
4244

43-
<p>Simply checking that the path is under a trusted location (such as a known public folder) is not enough,
44-
however, since the path could contain relative components such as "..". To fix this, check that it does
45-
not contain ".." and starts with the public folder.</p>
45+
<p>
46+
If the input should only be a file name, you can check that it doesn't contain any path separators or ".." sequences.
47+
</p>
4648

47-
<sample src="TaintedPathGood.java" />
49+
<sample src="examples/TaintedPathGoodNormalize.java" />
50+
51+
<p>
52+
If the input should be within a specific directory, you can check that the resolved path
53+
is still contained within that directory.
54+
</p>
55+
56+
<sample src="examples/TaintedPathGoodFolder.java" />
4857

4958
</example>
5059
<references>

java/ql/src/Security/CWE/CWE-022/TaintedPathGood.java

Lines changed: 0 additions & 14 deletions
This file was deleted.

java/ql/src/Security/CWE/CWE-022/ZipSlip.qhelp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ the result is within the destination directory. If provided with a zip file cont
4545
path like <code>..\sneaky-file</code>, then this file would be written outside the destination
4646
directory.</p>
4747

48-
<sample src="ZipSlipBad.java" />
48+
<sample src="examples/ZipSlipBad.java" />
4949

5050
<p>To fix this vulnerability, we need to verify that the normalized <code>file</code> still has
5151
<code>destinationDir</code> as its prefix, and throw an exception if this is not the case.</p>
5252

53-
<sample src="ZipSlipGood.java" />
53+
<sample src="examples/ZipSlipGood.java" />
5454

5555
</example>
5656
<references>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
public void sendUserFileGood(Socket sock, String user) {
2+
BufferedReader filenameReader = new BufferedReader(
3+
new InputStreamReader(sock.getInputStream(), "UTF-8"));
4+
String filename = filenameReader.readLine();
5+
6+
Path publicFolder = Paths.get("/home/" + user + "/public").normalize().toAbsolutePath();
7+
Path filePath = publicFolder.resolve(filename).normalize().toAbsolutePath();
8+
9+
// GOOD: ensure that the path stays within the public folder
10+
if (!filePath.startsWith(publicFolder + File.separator)) {
11+
throw new IllegalArgumentException("Invalid filename");
12+
}
13+
BufferedReader fileReader = new BufferedReader(new FileReader(filePath.toString()));
14+
String fileLine = fileReader.readLine();
15+
while(fileLine != null) {
16+
sock.getOutputStream().write(fileLine.getBytes());
17+
fileLine = fileReader.readLine();
18+
}
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
public void sendUserFileGood(Socket sock, String user) {
2+
BufferedReader filenameReader = new BufferedReader(
3+
new InputStreamReader(sock.getInputStream(), "UTF-8"));
4+
String filename = filenameReader.readLine();
5+
// GOOD: ensure that the filename has no path separators or parent directory references
6+
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
7+
throw new IllegalArgumentException("Invalid filename");
8+
}
9+
BufferedReader fileReader = new BufferedReader(new FileReader(filename));
10+
String fileLine = fileReader.readLine();
11+
while(fileLine != null) {
12+
sock.getOutputStream().write(fileLine.getBytes());
13+
fileLine = fileReader.readLine();
14+
}
15+
}

java/ql/test/query-tests/security/CWE-022/semmle/tests/TaintedPath.expected

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
edges
2-
| TaintedPath.java:11:38:11:110 | new BufferedReader(...) : BufferedReader | TaintedPath.java:12:24:12:37 | filenameReader : BufferedReader |
3-
| TaintedPath.java:11:57:11:109 | new InputStreamReader(...) : InputStreamReader | TaintedPath.java:11:38:11:110 | new BufferedReader(...) : BufferedReader |
4-
| TaintedPath.java:11:79:11:99 | getInputStream(...) : InputStream | TaintedPath.java:11:57:11:109 | new InputStreamReader(...) : InputStreamReader |
5-
| TaintedPath.java:12:24:12:37 | filenameReader : BufferedReader | TaintedPath.java:12:24:12:48 | readLine(...) : String |
6-
| TaintedPath.java:12:24:12:48 | readLine(...) : String | TaintedPath.java:14:68:14:75 | filename |
2+
| TaintedPath.java:12:38:12:110 | new BufferedReader(...) : BufferedReader | TaintedPath.java:13:24:13:37 | filenameReader : BufferedReader |
3+
| TaintedPath.java:12:57:12:109 | new InputStreamReader(...) : InputStreamReader | TaintedPath.java:12:38:12:110 | new BufferedReader(...) : BufferedReader |
4+
| TaintedPath.java:12:79:12:99 | getInputStream(...) : InputStream | TaintedPath.java:12:57:12:109 | new InputStreamReader(...) : InputStreamReader |
5+
| TaintedPath.java:13:24:13:37 | filenameReader : BufferedReader | TaintedPath.java:13:24:13:48 | readLine(...) : String |
6+
| TaintedPath.java:13:24:13:48 | readLine(...) : String | TaintedPath.java:15:68:15:75 | filename |
7+
| TaintedPath.java:38:41:39:70 | new BufferedReader(...) : BufferedReader | TaintedPath.java:40:27:40:40 | filenameReader : BufferedReader |
8+
| TaintedPath.java:39:17:39:69 | new InputStreamReader(...) : InputStreamReader | TaintedPath.java:38:41:39:70 | new BufferedReader(...) : BufferedReader |
9+
| TaintedPath.java:39:39:39:59 | getInputStream(...) : InputStream | TaintedPath.java:39:17:39:69 | new InputStreamReader(...) : InputStreamReader |
10+
| TaintedPath.java:40:27:40:40 | filenameReader : BufferedReader | TaintedPath.java:40:27:40:51 | readLine(...) : String |
11+
| TaintedPath.java:40:27:40:51 | readLine(...) : String | TaintedPath.java:43:46:43:53 | filename |
712
| Test.java:19:18:19:38 | getHostName(...) : String | Test.java:24:20:24:23 | temp |
813
| Test.java:19:18:19:38 | getHostName(...) : String | Test.java:27:21:27:24 | temp |
914
| Test.java:19:18:19:38 | getHostName(...) : String | Test.java:30:44:30:47 | temp |
@@ -189,12 +194,18 @@ edges
189194
| mad/Test.java:221:26:221:33 | source(...) : String | mad/Test.java:221:19:221:33 | (...)... |
190195
| mad/Test.java:226:29:226:36 | source(...) : String | mad/Test.java:226:20:226:36 | (...)... |
191196
nodes
192-
| TaintedPath.java:11:38:11:110 | new BufferedReader(...) : BufferedReader | semmle.label | new BufferedReader(...) : BufferedReader |
193-
| TaintedPath.java:11:57:11:109 | new InputStreamReader(...) : InputStreamReader | semmle.label | new InputStreamReader(...) : InputStreamReader |
194-
| TaintedPath.java:11:79:11:99 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
195-
| TaintedPath.java:12:24:12:37 | filenameReader : BufferedReader | semmle.label | filenameReader : BufferedReader |
196-
| TaintedPath.java:12:24:12:48 | readLine(...) : String | semmle.label | readLine(...) : String |
197-
| TaintedPath.java:14:68:14:75 | filename | semmle.label | filename |
197+
| TaintedPath.java:12:38:12:110 | new BufferedReader(...) : BufferedReader | semmle.label | new BufferedReader(...) : BufferedReader |
198+
| TaintedPath.java:12:57:12:109 | new InputStreamReader(...) : InputStreamReader | semmle.label | new InputStreamReader(...) : InputStreamReader |
199+
| TaintedPath.java:12:79:12:99 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
200+
| TaintedPath.java:13:24:13:37 | filenameReader : BufferedReader | semmle.label | filenameReader : BufferedReader |
201+
| TaintedPath.java:13:24:13:48 | readLine(...) : String | semmle.label | readLine(...) : String |
202+
| TaintedPath.java:15:68:15:75 | filename | semmle.label | filename |
203+
| TaintedPath.java:38:41:39:70 | new BufferedReader(...) : BufferedReader | semmle.label | new BufferedReader(...) : BufferedReader |
204+
| TaintedPath.java:39:17:39:69 | new InputStreamReader(...) : InputStreamReader | semmle.label | new InputStreamReader(...) : InputStreamReader |
205+
| TaintedPath.java:39:39:39:59 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
206+
| TaintedPath.java:40:27:40:40 | filenameReader : BufferedReader | semmle.label | filenameReader : BufferedReader |
207+
| TaintedPath.java:40:27:40:51 | readLine(...) : String | semmle.label | readLine(...) : String |
208+
| TaintedPath.java:43:46:43:53 | filename | semmle.label | filename |
198209
| Test.java:19:18:19:38 | getHostName(...) : String | semmle.label | getHostName(...) : String |
199210
| Test.java:24:20:24:23 | temp | semmle.label | temp |
200211
| Test.java:27:21:27:24 | temp | semmle.label | temp |
@@ -386,7 +397,8 @@ nodes
386397
| mad/Test.java:226:29:226:36 | source(...) : String | semmle.label | source(...) : String |
387398
subpaths
388399
#select
389-
| TaintedPath.java:14:53:14:76 | new FileReader(...) | TaintedPath.java:11:79:11:99 | getInputStream(...) : InputStream | TaintedPath.java:14:68:14:75 | filename | This path depends on a $@. | TaintedPath.java:11:79:11:99 | getInputStream(...) | user-provided value |
400+
| TaintedPath.java:15:53:15:76 | new FileReader(...) | TaintedPath.java:12:79:12:99 | getInputStream(...) : InputStream | TaintedPath.java:15:68:15:75 | filename | This path depends on a $@. | TaintedPath.java:12:79:12:99 | getInputStream(...) | user-provided value |
401+
| TaintedPath.java:43:25:43:54 | resolve(...) | TaintedPath.java:39:39:39:59 | getInputStream(...) : InputStream | TaintedPath.java:43:46:43:53 | filename | This path depends on a $@. | TaintedPath.java:39:39:39:59 | getInputStream(...) | user-provided value |
390402
| Test.java:24:11:24:24 | new File(...) | Test.java:19:18:19:38 | getHostName(...) : String | Test.java:24:20:24:23 | temp | This path depends on a $@. | Test.java:19:18:19:38 | getHostName(...) | user-provided value |
391403
| Test.java:27:11:27:25 | get(...) | Test.java:19:18:19:38 | getHostName(...) : String | Test.java:27:21:27:24 | temp | This path depends on a $@. | Test.java:19:18:19:38 | getHostName(...) | user-provided value |
392404
| Test.java:30:11:30:48 | getPath(...) | Test.java:19:18:19:38 | getHostName(...) : String | Test.java:30:44:30:47 | temp | This path depends on a $@. | Test.java:19:18:19:38 | getHostName(...) | user-provided value |

java/ql/test/query-tests/security/CWE-022/semmle/tests/TaintedPath.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import java.io.BufferedReader;
2+
import java.io.File;
23
import java.io.FileReader;
4+
import java.io.IOException;
35
import java.io.InputStreamReader;
46
import java.net.Socket;
57
import java.nio.file.Path;
68
import java.nio.file.Paths;
7-
import java.io.IOException;
89

910
public class TaintedPath {
1011
public void sendUserFile(Socket sock, String user) throws IOException {
@@ -32,4 +33,40 @@ public void sendUserFileGood(Socket sock, String user) throws IOException {
3233
}
3334
}
3435
}
36+
37+
public void sendUserFileGood2(Socket sock, String user) throws Exception {
38+
BufferedReader filenameReader = new BufferedReader(
39+
new InputStreamReader(sock.getInputStream(), "UTF-8"));
40+
String filename = filenameReader.readLine();
41+
42+
Path publicFolder = Paths.get("/home/" + user + "/public").normalize().toAbsolutePath();
43+
Path filePath = publicFolder.resolve(filename).normalize().toAbsolutePath(); // FP until the path-injection sinks are reworked
44+
45+
// GOOD: ensure that the path stays within the public folder
46+
if (!filePath.startsWith(publicFolder + File.separator)) {
47+
throw new IllegalArgumentException("Invalid filename");
48+
}
49+
BufferedReader fileReader = new BufferedReader(new FileReader(filePath.toString()));
50+
String fileLine = fileReader.readLine();
51+
while(fileLine != null) {
52+
sock.getOutputStream().write(fileLine.getBytes());
53+
fileLine = fileReader.readLine();
54+
}
55+
}
56+
57+
public void sendUserFileGood3(Socket sock, String user) throws Exception {
58+
BufferedReader filenameReader = new BufferedReader(
59+
new InputStreamReader(sock.getInputStream(), "UTF-8"));
60+
String filename = filenameReader.readLine();
61+
// GOOD: ensure that the filename has no path separators or parent directory references
62+
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
63+
throw new IllegalArgumentException("Invalid filename");
64+
}
65+
BufferedReader fileReader = new BufferedReader(new FileReader(filename));
66+
String fileLine = fileReader.readLine();
67+
while(fileLine != null) {
68+
sock.getOutputStream().write(fileLine.getBytes());
69+
fileLine = fileReader.readLine();
70+
}
71+
}
3572
}

0 commit comments

Comments
 (0)