Skip to content

Commit ca33402

Browse files
authored
Merge pull request github#14793 from github/max-schaefer/tainted-path-qhelp
Java: Improve QHelp for `java/path-injection` to mention less disruptive fixes.
2 parents 69ab389 + a5e7ef4 commit ca33402

File tree

6 files changed

+97
-28
lines changed

6 files changed

+97
-28
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: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,44 @@ can result in sensitive information being revealed or deleted, or an attacker be
88
behavior by modifying unexpected files.</p>
99

1010
<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 to any directory on the file system.</p>
11+
such as "..". Such a path may potentially point anywhere on the file system.</p>
1212

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 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>
20+
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>
25+
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>
30+
31+
<p>Finally, the simplest (but most restrictive) option is to use an allow list of safe patterns and make sure that
32+
the user input matches one of these patterns.</p>
2533

2634
</recommendation>
2735
<example>
2836

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>
37+
<p>In this example, a file name is read from a <code>java.net.Socket</code> and then used to access a file
38+
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>
3440

3541
<sample src="TaintedPath.java" />
3642

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>
46+
47+
<sample src="TaintedPathGood.java" />
48+
3749
</example>
3850
<references>
3951

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 (!filename.contains("..") && filename.startsWith("/home/" + user + "/public/")) {
7+
BufferedReader fileReader = new BufferedReader(new FileReader(filename));
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 filename = filenameReader.readLine();
25+
// GOOD: ensure that the file is in a designated folder in the user's home directory
26+
if (!filename.contains("..") && filename.startsWith("/home/" + user + "/public/")) {
27+
BufferedReader fileReader = new BufferedReader(new FileReader(filename));
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)