Skip to content

Commit 074583e

Browse files
authored
add archive api file open query and test
1 parent 4a02505 commit 074583e

File tree

4 files changed

+132
-0
lines changed

4 files changed

+132
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* @name User-controlled filename in archive library
3+
* @description User-controlled data that flows into File I/O of archive libraries could be dangerous
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 7.0
7+
* @precision medium
8+
* @id rb/user-controlled-path-traversal-archive-library
9+
* @tags security external/cwe/cwe-22
10+
*/
11+
12+
import ruby
13+
import codeql.ruby.ApiGraphs
14+
import codeql.ruby.DataFlow
15+
import codeql.ruby.dataflow.RemoteFlowSources
16+
import codeql.ruby.dataflow.BarrierGuards
17+
import codeql.ruby.TaintTracking
18+
import DataFlow::PathGraph
19+
20+
class ArchiveApiFileOpen extends DataFlow::Node {
21+
22+
// this should find the first argument of Zlib::Inflate.inflate or Zip::File.extract
23+
ArchiveApiFileOpen() {
24+
this instanceof RubyZipFileOpen or
25+
this instanceof TarReaderFileOpen
26+
}
27+
28+
}
29+
30+
class RubyZipFileOpen extends DataFlow::Node {
31+
// find any use of Zip::File.open()
32+
RubyZipFileOpen() {
33+
this = API::getTopLevelMember("Zip").getMember("File").getAMethodCall("open").getArgument(0)
34+
}
35+
}
36+
37+
class TarReaderFileOpen extends DataFlow::Node {
38+
// this should find a use of File.open() in context of a block opened in context of Gem::Package::TarReader.new()
39+
TarReaderFileOpen() {
40+
this = API::getTopLevelMember("File").getAMethodCall("open").getArgument(0) and
41+
this.asExpr().getExpr().getParent+() = API::getTopLevelMember("Gem").getMember("Package").getMember("TarReader").getAnInstantiation().asExpr().getExpr()
42+
}
43+
}
44+
45+
class Configuration extends TaintTracking::Configuration {
46+
Configuration() { this = "ArchiveApiFileOpen" }
47+
48+
// this predicate will be used to contstrain our query to find instances where only remote user-controlled data flows to the sink
49+
override predicate isSource(DataFlow::Node source) {
50+
source instanceof RemoteFlowSource
51+
}
52+
53+
// our Decompression APIs defined above will the the sinks we use for this query
54+
override predicate isSink(DataFlow::Node sink) {
55+
sink instanceof ArchiveApiFileOpen
56+
}
57+
58+
// I think it would also be helpful to reduce false positives by adding a simple sanitizer config in the event
59+
// that the code first checks the file name against a string literal or array of string literals before calling
60+
// the decompression API
61+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
62+
guard instanceof StringConstCompare or
63+
guard instanceof StringConstArrayInclusionCall
64+
}
65+
}
66+
67+
68+
from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
69+
where
70+
config.hasFlowPath(source, sink)
71+
select sink.getNode(), source, sink, "This call to $@ appears to extract an archive using user-controlled data $@ to set the filename. If the filename is not properly handled, they could end up writing to unintended places in the filesystem.", sink.getNode().asExpr().getExpr().getParent().toString(), sink.getNode().asExpr().getExpr().getParent().toString(), source.toString(), source.toString()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
edges
2+
| ArchiveApiPathTraversal.rb:4:26:4:31 | call to params : | ArchiveApiPathTraversal.rb:4:26:4:42 | ...[...] : |
3+
| ArchiveApiPathTraversal.rb:4:26:4:42 | ...[...] : | ArchiveApiPathTraversal.rb:11:17:11:27 | destination : |
4+
| ArchiveApiPathTraversal.rb:8:11:8:16 | call to params : | ArchiveApiPathTraversal.rb:8:11:8:23 | ...[...] : |
5+
| ArchiveApiPathTraversal.rb:8:11:8:23 | ...[...] : | ArchiveApiPathTraversal.rb:29:13:29:16 | file : |
6+
| ArchiveApiPathTraversal.rb:11:17:11:27 | destination : | ArchiveApiPathTraversal.rb:14:38:14:48 | destination : |
7+
| ArchiveApiPathTraversal.rb:14:28:14:67 | call to join : | ArchiveApiPathTraversal.rb:21:21:21:36 | destination_file |
8+
| ArchiveApiPathTraversal.rb:14:38:14:48 | destination : | ArchiveApiPathTraversal.rb:14:28:14:67 | call to join : |
9+
| ArchiveApiPathTraversal.rb:29:13:29:16 | file : | ArchiveApiPathTraversal.rb:30:20:30:23 | file |
10+
nodes
11+
| ArchiveApiPathTraversal.rb:4:26:4:31 | call to params : | semmle.label | call to params : |
12+
| ArchiveApiPathTraversal.rb:4:26:4:42 | ...[...] : | semmle.label | ...[...] : |
13+
| ArchiveApiPathTraversal.rb:8:11:8:16 | call to params : | semmle.label | call to params : |
14+
| ArchiveApiPathTraversal.rb:8:11:8:23 | ...[...] : | semmle.label | ...[...] : |
15+
| ArchiveApiPathTraversal.rb:11:17:11:27 | destination : | semmle.label | destination : |
16+
| ArchiveApiPathTraversal.rb:14:28:14:67 | call to join : | semmle.label | call to join : |
17+
| ArchiveApiPathTraversal.rb:14:38:14:48 | destination : | semmle.label | destination : |
18+
| ArchiveApiPathTraversal.rb:21:21:21:36 | destination_file | semmle.label | destination_file |
19+
| ArchiveApiPathTraversal.rb:29:13:29:16 | file : | semmle.label | file : |
20+
| ArchiveApiPathTraversal.rb:30:20:30:23 | file | semmle.label | file |
21+
subpaths
22+
#select
23+
| ArchiveApiPathTraversal.rb:21:21:21:36 | destination_file | ArchiveApiPathTraversal.rb:4:26:4:31 | call to params : | ArchiveApiPathTraversal.rb:21:21:21:36 | destination_file | This call to $@ appears to extract an archive using user-controlled data $@ to set the filename. If the filename is not properly handled, they could end up writing to unintended places in the filesystem. | call to open | call to open | call to params : | call to params : |
24+
| ArchiveApiPathTraversal.rb:30:20:30:23 | file | ArchiveApiPathTraversal.rb:8:11:8:16 | call to params : | ArchiveApiPathTraversal.rb:30:20:30:23 | file | This call to $@ appears to extract an archive using user-controlled data $@ to set the filename. If the filename is not properly handled, they could end up writing to unintended places in the filesystem. | call to open | call to open | call to params : | call to params : |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/archive-api-path-traversal/ArchiveApiPathTraversal.ql
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class TestContoller < ActionController::Base
2+
3+
def upload
4+
untar params[:file], params[:filename]
5+
end
6+
7+
def unpload_zip
8+
unzip params[:file]
9+
end
10+
11+
def untar(io, destination)
12+
Gem::Package::TarReader.new io do |tar|
13+
tar.each do |tarfile|
14+
destination_file = File.join destination, tarfile.full_name
15+
16+
if tarfile.directory?
17+
FileUtils.mkdir_p destination_file
18+
else
19+
destination_directory = File.dirname(destination_file)
20+
FileUtils.mkdir_p destination_directory unless File.directory?(destination_directory)
21+
File.open destination_file, "wb" do |f|
22+
f.print tarfile.read
23+
end
24+
end
25+
end
26+
end
27+
end
28+
29+
def unzip(file)
30+
Zip::File.open(file) do |zip_file|
31+
zip_file.each do |entry|
32+
entry.extract
33+
end
34+
end
35+
end
36+
end

0 commit comments

Comments
 (0)