Skip to content

Commit 2dd280a

Browse files
committed
update the QHelp for rb/path-injection
1 parent 9be2b9c commit 2dd280a

File tree

5 files changed

+129
-36
lines changed

5 files changed

+129
-36
lines changed

ruby/ql/src/queries/security/cwe-022/PathInjection.qhelp

+43-36
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,67 @@
22
"-//Semmle//qhelp//EN"
33
"qhelp.dtd">
44
<qhelp>
5-
65
<overview>
7-
<p>
8-
Accessing files using paths constructed from user-controlled data can allow an
9-
attacker to access unexpected resources. This can result in sensitive
10-
information being revealed or deleted, or an attacker being able to influence
11-
behavior by modifying unexpected files.
12-
</p>
13-
</overview>
6+
<p>Accessing paths controlled by users can allow an attacker to access unexpected resources. This
7+
can result in sensitive information being revealed or deleted, or an attacker being able to influence
8+
behavior by modifying unexpected files.</p>
9+
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>
1412

13+
</overview>
1514
<recommendation>
16-
<p>
17-
Validate user input before using it to construct a file path, either using an
18-
off-the-shelf library like <code>ActiveStorage::Filename#sanitized</code> in
19-
Rails, or by performing custom validation.
15+
16+
<p>Validate user input before using it to construct a file path.</p>
17+
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>
22+
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.
2025
</p>
2126

2227
<p>
23-
Ideally, follow these rules:
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.
2431
</p>
2532

26-
<ul>
27-
<li>Do not allow more than a single "." character.</li>
28-
<li>Do not allow directory separators such as "/" or "\" (depending on the file
29-
system).</li>
30-
<li>Do not rely on simply replacing problematic sequences such as "../". For
31-
example, after applying this filter to ".../...//", the resulting string would
32-
still be "../".</li>
33-
<li>Use a whitelist of known good patterns.</li>
34-
</ul>
35-
</recommendation>
33+
<p>Finally, the simplest (but most restrictive) option is to use an allow list of safe patterns and make sure that
34+
the user input matches one of these patterns.</p>
3635

36+
</recommendation>
3737
<example>
38+
39+
<p>In this example, a file name is read from a query parameter and then used to access a file
40+
and send it back over the socket. However, a malicious user could enter a file name anywhere on the file system,
41+
such as "/etc/passwd" or "../../../etc/passwd".</p>
42+
43+
<sample src="examples/tainted_path.rb" />
44+
3845
<p>
39-
In the first example, a file name is read from an HTTP request and then used to
40-
access a file. However, a malicious user could enter a file name which is an
41-
absolute path, such as <code>"/etc/passwd"</code>.
46+
If the input should only be a file name, you can check that it doesn't contain any path separators or ".." sequences.
4247
</p>
4348

49+
<sample src="examples/tainted_path_normalize.rb" />
50+
4451
<p>
45-
In the second example, it appears that the user is restricted to opening a file
46-
within the <code>"user"</code> home directory. However, a malicious user could
47-
enter a file name containing special characters. For example, the string
48-
<code>"../../etc/passwd"</code> will result in the code reading the file located
49-
at <code>"/home/user/../../etc/passwd"</code>, which is the system's password
50-
file. This file would then be sent back to the user, giving them access to all
51-
the system's passwords.
52+
If the input should be within a specific directory, you can check that the resolved path
53+
is still contained within that directory.
5254
</p>
5355

54-
<sample src="examples/tainted_path.rb" />
55-
</example>
56+
<sample src="examples/tainted_path_folder.rb" />
5657

58+
</example>
5759
<references>
58-
<li>OWASP: <a href="https://owasp.org/www-community/attacks/Path_Traversal">Path Traversal</a>.</li>
60+
61+
<li>
62+
OWASP:
63+
<a href="https://owasp.org/www-community/attacks/Path_Traversal">Path Traversal</a>.
5964
<li>Rails: <a href="https://api.rubyonrails.org/classes/ActiveStorage/Filename.html#method-i-sanitized">ActiveStorage::Filename#sanitized</a>.</li>
65+
</li>
66+
6067
</references>
6168
</qhelp>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
require 'pathname'
2+
3+
class FilesController < ActionController::Base
4+
def safe_example
5+
filename = params[:path]
6+
user_directory = "/home/#{current_user}/public" # Assuming `current_user` method returns the user's name
7+
8+
public_folder = Pathname.new(user_directory).cleanpath.to_s
9+
file_path = Pathname.new(File.join(user_directory, filename)).cleanpath.to_s
10+
11+
# GOOD: Ensure that the path stays within the public folder
12+
if !file_path.start_with?(public_folder + File::SEPARATOR)
13+
raise ArgumentError, "Invalid filename"
14+
else
15+
@content = File.read(file_path)
16+
end
17+
end
18+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class FilesController < ActionController::Base
2+
def safe_example
3+
filename = params[:path]
4+
# GOOD: Ensure that the filename has no path separators or parent directory references
5+
if filename.include?("..") || filename.include?("/") || filename.include?("\\")
6+
raise ArgumentError, "Invalid filename"
7+
else
8+
# Assuming files are stored in a specific directory, e.g., /home/user/files/
9+
@content = File.read("/home/user/files/#{filename}")
10+
end
11+
end
12+
end

ruby/ql/test/query-tests/security/cwe-022/PathInjection.expected

+28
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ edges
6666
| tainted_path.rb:90:12:90:53 | call to new | tainted_path.rb:90:5:90:8 | path | provenance | |
6767
| tainted_path.rb:90:40:90:45 | call to params | tainted_path.rb:90:40:90:52 | ...[...] | provenance | |
6868
| tainted_path.rb:90:40:90:52 | ...[...] | tainted_path.rb:90:12:90:53 | call to new | provenance | |
69+
| tainted_path.rb:98:5:98:12 | filename | tainted_path.rb:102:56:102:63 | filename | provenance | |
70+
| tainted_path.rb:98:16:98:21 | call to params | tainted_path.rb:98:16:98:28 | ...[...] | provenance | |
71+
| tainted_path.rb:98:16:98:28 | ...[...] | tainted_path.rb:98:5:98:12 | filename | provenance | |
72+
| tainted_path.rb:102:5:102:13 | file_path | tainted_path.rb:108:28:108:36 | file_path | provenance | |
73+
| tainted_path.rb:102:17:102:65 | call to new | tainted_path.rb:102:17:102:75 | call to cleanpath | provenance | |
74+
| tainted_path.rb:102:17:102:75 | call to cleanpath | tainted_path.rb:102:17:102:80 | call to to_s | provenance | |
75+
| tainted_path.rb:102:17:102:80 | call to to_s | tainted_path.rb:102:5:102:13 | file_path | provenance | |
76+
| tainted_path.rb:102:30:102:64 | call to join | tainted_path.rb:102:17:102:65 | call to new | provenance | |
77+
| tainted_path.rb:102:56:102:63 | filename | tainted_path.rb:102:30:102:64 | call to join | provenance | |
78+
| tainted_path.rb:113:5:113:12 | filename | tainted_path.rb:119:28:119:57 | "/home/user/files/#{...}" | provenance | |
79+
| tainted_path.rb:113:16:113:21 | call to params | tainted_path.rb:113:16:113:28 | ...[...] | provenance | |
80+
| tainted_path.rb:113:16:113:28 | ...[...] | tainted_path.rb:113:5:113:12 | filename | provenance | |
6981
nodes
7082
| ArchiveApiPathTraversal.rb:5:26:5:31 | call to params | semmle.label | call to params |
7183
| ArchiveApiPathTraversal.rb:5:26:5:42 | ...[...] | semmle.label | ...[...] |
@@ -150,6 +162,20 @@ nodes
150162
| tainted_path.rb:90:40:90:45 | call to params | semmle.label | call to params |
151163
| tainted_path.rb:90:40:90:52 | ...[...] | semmle.label | ...[...] |
152164
| tainted_path.rb:92:11:92:14 | path | semmle.label | path |
165+
| tainted_path.rb:98:5:98:12 | filename | semmle.label | filename |
166+
| tainted_path.rb:98:16:98:21 | call to params | semmle.label | call to params |
167+
| tainted_path.rb:98:16:98:28 | ...[...] | semmle.label | ...[...] |
168+
| tainted_path.rb:102:5:102:13 | file_path | semmle.label | file_path |
169+
| tainted_path.rb:102:17:102:65 | call to new | semmle.label | call to new |
170+
| tainted_path.rb:102:17:102:75 | call to cleanpath | semmle.label | call to cleanpath |
171+
| tainted_path.rb:102:17:102:80 | call to to_s | semmle.label | call to to_s |
172+
| tainted_path.rb:102:30:102:64 | call to join | semmle.label | call to join |
173+
| tainted_path.rb:102:56:102:63 | filename | semmle.label | filename |
174+
| tainted_path.rb:108:28:108:36 | file_path | semmle.label | file_path |
175+
| tainted_path.rb:113:5:113:12 | filename | semmle.label | filename |
176+
| tainted_path.rb:113:16:113:21 | call to params | semmle.label | call to params |
177+
| tainted_path.rb:113:16:113:28 | ...[...] | semmle.label | ...[...] |
178+
| tainted_path.rb:119:28:119:57 | "/home/user/files/#{...}" | semmle.label | "/home/user/files/#{...}" |
153179
subpaths
154180
#select
155181
| ArchiveApiPathTraversal.rb:59:21:59:36 | destination_file | ArchiveApiPathTraversal.rb:5:26:5:31 | call to params | ArchiveApiPathTraversal.rb:59:21:59:36 | destination_file | This path depends on a $@. | ArchiveApiPathTraversal.rb:5:26:5:31 | call to params | user-provided value |
@@ -170,3 +196,5 @@ subpaths
170196
| tainted_path.rb:85:10:85:13 | path | tainted_path.rb:84:40:84:45 | call to params | tainted_path.rb:85:10:85:13 | path | This path depends on a $@. | tainted_path.rb:84:40:84:45 | call to params | user-provided value |
171197
| tainted_path.rb:86:25:86:28 | path | tainted_path.rb:84:40:84:45 | call to params | tainted_path.rb:86:25:86:28 | path | This path depends on a $@. | tainted_path.rb:84:40:84:45 | call to params | user-provided value |
172198
| tainted_path.rb:92:11:92:14 | path | tainted_path.rb:90:40:90:45 | call to params | tainted_path.rb:92:11:92:14 | path | This path depends on a $@. | tainted_path.rb:90:40:90:45 | call to params | user-provided value |
199+
| tainted_path.rb:108:28:108:36 | file_path | tainted_path.rb:98:16:98:21 | call to params | tainted_path.rb:108:28:108:36 | file_path | This path depends on a $@. | tainted_path.rb:98:16:98:21 | call to params | user-provided value |
200+
| tainted_path.rb:119:28:119:57 | "/home/user/files/#{...}" | tainted_path.rb:113:16:113:21 | call to params | tainted_path.rb:119:28:119:57 | "/home/user/files/#{...}" | This path depends on a $@. | tainted_path.rb:113:16:113:21 | call to params | user-provided value |

ruby/ql/test/query-tests/security/cwe-022/tainted_path.rb

+28
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,32 @@ def require_relative()
9191
puts "Debug: require_relative(#{path})"
9292
super(path)
9393
end
94+
95+
require 'pathname'
96+
97+
def safe_example_folder()
98+
filename = params[:path]
99+
user_directory = "/home/#{current_user}/public" # Assuming `current_user` method returns the user's name
100+
101+
public_folder = Pathname.new(user_directory).cleanpath.to_s
102+
file_path = Pathname.new(File.join(user_directory, filename)).cleanpath.to_s
103+
104+
# GOOD: Ensure that the path stays within the public folder
105+
if !file_path.start_with?(public_folder + File::SEPARATOR)
106+
raise ArgumentError, "Invalid filename"
107+
else
108+
@content = File.read(file_path) # GOOD, but we falsely report a vulnerability
109+
end
110+
end
111+
112+
def safe_example_normalize()
113+
filename = params[:path]
114+
# GOOD: Ensure that the filename has no path separators or parent directory references
115+
if filename.include?("..") || filename.include?("/") || filename.include?("\\")
116+
raise ArgumentError, "Invalid filename"
117+
else
118+
# Assuming files are stored in a specific directory, e.g., /home/user/files/
119+
@content = File.read("/home/user/files/#{filename}") # GOOD, but we falsely report a vulnerability
120+
end
121+
end
94122
end

0 commit comments

Comments
 (0)