Skip to content

Commit a46a7fa

Browse files
author
Max Schaefer
committed
Java: Improve QHelp for java/path-injection to mention less disruptive fixes.
1 parent 104700f commit a46a7fa

File tree

6 files changed

+93
-27
lines changed

6 files changed

+93
-27
lines changed

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

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,11 @@ public void sendUserFile(Socket sock, String user) {
22
BufferedReader filenameReader = new BufferedReader(
33
new InputStreamReader(sock.getInputStream(), "UTF-8"));
44
String filename = filenameReader.readLine();
5-
// BAD: read from a file using a path controlled by the user
6-
BufferedReader fileReader = new BufferedReader(
7-
new FileReader("/home/" + user + "/" + filename));
5+
// BAD: read from a file without checking its path
6+
BufferedReader fileReader = new BufferedReader(new FileReader(filename));
87
String fileLine = fileReader.readLine();
98
while(fileLine != null) {
109
sock.getOutputStream().write(fileLine.getBytes());
1110
fileLine = fileReader.readLine();
1211
}
1312
}
14-
15-
public void sendUserFileFixed(Socket sock, String user) {
16-
// ...
17-
18-
// GOOD: remove all dots and directory delimiters from the filename before using
19-
String filename = filenameReader.readLine().replaceAll("\\.", "").replaceAll("/", "");
20-
BufferedReader fileReader = new BufferedReader(
21-
new FileReader("/home/" + user + "/" + filename));
22-
23-
// ...
24-
}

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

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,36 @@ such as "..". Such a path may potentially point to any directory on the file sys
1313
</overview>
1414
<recommendation>
1515

16-
<p>Validate user input before using it to construct a file path. Ideally, follow these rules:</p>
16+
<p>Validate user input before using it to construct a file path.</p>
1717

18-
<ul>
19-
<li>Do not allow more than a single "." character.</li>
20-
<li>Do not allow directory separators such as "/" or "\" (depending on the file system).</li>
21-
<li>Do not rely on simply replacing problematic sequences such as "../". For example, after applying this filter to
22-
".../...//" the resulting string would still be "../".</li>
23-
<li>Ideally use a whitelist of known good patterns.</li>
24-
</ul>
18+
<p>The choice of validation depends on the use case.</p>
19+
20+
<p>If you want to allow paths spanning multiple folders, a common strategy is to make sure that the constructed
21+
file path is contained within a safe root folder, for example by checking that the path starts with the root folder.
22+
Additionally, you need to ensure that the path does not contain any ".." components, since these would allow
23+
the path to escape the root folder.</p>
24+
25+
<p>A safer (but more restrictive) option is to use an allow list of safe paths and make sure that
26+
the user input is one of those paths.</p>
2527

2628
</recommendation>
2729
<example>
2830

29-
<p>In this example, a file name is read from a <code>java.net.Socket</code> and then used to access a file in the
30-
user's home directory and send it back over the socket. However, a malicious user could enter a file name which contains special
31-
characters. For example, the string "../../etc/passwd" will result in the code reading the file located at
32-
"/home/[user]/../../etc/passwd", which is the system's password file. This file would then be sent back to the user,
33-
giving them access to all the system's passwords.</p>
31+
<p>In this example, a file name is read from a <code>java.net.Socket</code> and then used to access a file
32+
and send it back over the socket. However, a malicious user could enter a file name anywhere on the file system,
33+
such as "/etc/passwd".</p>
3434

3535
<sample src="TaintedPath.java" />
3636

37+
<p>Simply checking that the path is under a trusted location (such as the user's home directory) is not enough,
38+
however, since the path could contain relative components such as "..". For example, the string
39+
"/home/[user]/../../etc/passwd" starts with the user's home directory, but would still result in the code reading
40+
the file located at "/etc/passwd".</p>
41+
42+
<p>To fix this, we check that the user-provided path does not contain ".." and starts with the user's home directory.</p>
43+
44+
<sample src="TaintedPathGood.java" />
45+
3746
</example>
3847
<references>
3948

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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 file is in a designated folder in the user's home directory
6+
if (!filePath.contains("..") && filePath.startsWith("/home/" + user + "/public/")) {
7+
BufferedReader fileReader = new BufferedReader(new FileReader(filePath));
8+
String fileLine = fileReader.readLine();
9+
while(fileLine != null) {
10+
sock.getOutputStream().write(fileLine.getBytes());
11+
fileLine = fileReader.readLine();
12+
}
13+
}
14+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
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 |
27
| Test.java:19:18:19:38 | getHostName(...) : String | Test.java:24:20:24:23 | temp |
38
| Test.java:19:18:19:38 | getHostName(...) : String | Test.java:27:21:27:24 | temp |
49
| Test.java:19:18:19:38 | getHostName(...) : String | Test.java:30:44:30:47 | temp |
@@ -184,6 +189,12 @@ edges
184189
| mad/Test.java:221:26:221:33 | source(...) : String | mad/Test.java:221:19:221:33 | (...)... |
185190
| mad/Test.java:226:29:226:36 | source(...) : String | mad/Test.java:226:20:226:36 | (...)... |
186191
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 |
187198
| Test.java:19:18:19:38 | getHostName(...) : String | semmle.label | getHostName(...) : String |
188199
| Test.java:24:20:24:23 | temp | semmle.label | temp |
189200
| Test.java:27:21:27:24 | temp | semmle.label | temp |
@@ -375,6 +386,7 @@ nodes
375386
| mad/Test.java:226:29:226:36 | source(...) : String | semmle.label | source(...) : String |
376387
subpaths
377388
#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 |
378390
| 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 |
379391
| 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 |
380392
| 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 |
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import java.io.BufferedReader;
2+
import java.io.FileReader;
3+
import java.io.InputStreamReader;
4+
import java.net.Socket;
5+
import java.nio.file.Path;
6+
import java.nio.file.Paths;
7+
import java.io.IOException;
8+
9+
public class TaintedPath {
10+
public void sendUserFile(Socket sock, String user) throws IOException {
11+
BufferedReader filenameReader = new BufferedReader(new InputStreamReader(sock.getInputStream(), "UTF-8"));
12+
String filename = filenameReader.readLine();
13+
// BAD: read from a file without checking its path
14+
BufferedReader fileReader = new BufferedReader(new FileReader(filename));
15+
String fileLine = fileReader.readLine();
16+
while(fileLine != null) {
17+
sock.getOutputStream().write(fileLine.getBytes());
18+
fileLine = fileReader.readLine();
19+
}
20+
}
21+
22+
public void sendUserFileGood(Socket sock, String user) throws IOException {
23+
BufferedReader filenameReader = new BufferedReader(new InputStreamReader(sock.getInputStream(), "UTF-8"));
24+
String filePath = filenameReader.readLine();
25+
// GOOD: ensure that the file is in a designated folder in the user's home directory
26+
if (!filePath.contains("..") && filePath.startsWith("/home/" + user + "/public/")) {
27+
BufferedReader fileReader = new BufferedReader(new FileReader(filePath));
28+
String fileLine = fileReader.readLine();
29+
while(fileLine != null) {
30+
sock.getOutputStream().write(fileLine.getBytes());
31+
fileLine = fileReader.readLine();
32+
}
33+
}
34+
}
35+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,12 @@ void doGet5(InetAddress address)
101101
new File(new URI(null, null, null, 0, t, null, null));
102102
}
103103

104+
void doGet6(String root, InetAddress address)
105+
throws IOException{
106+
String temp = address.getHostName();
107+
// GOOD: Use `contains` and `startsWith` to check if the path is safe
108+
if (!temp.contains("..") && temp.startsWith(root + "/")) {
109+
File file = new File(temp);
110+
}
111+
}
104112
}

0 commit comments

Comments
 (0)