Merge pull request #9477 from thiggy1342/experimental-archive-api

RB: Adding experimental query for detecting path traversal in Archive libraries
This commit is contained in:
Arthur Baars
2022-06-16 17:45:18 +02:00
committed by GitHub
8 changed files with 164 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
## 0.2.3
### Minor Analysis Improvements
- Calls to `Zip::File.open` and `Zip::File.new` have been added as `FileSystemAccess` sinks. As a result queries like `rb/path-injection` now flag up cases where users may access arbitrary archive files.

View File

@@ -8,6 +8,7 @@ private import codeql.ruby.frameworks.ActiveRecord
private import codeql.ruby.frameworks.ActiveStorage
private import codeql.ruby.frameworks.ActionView
private import codeql.ruby.frameworks.ActiveSupport
private import codeql.ruby.frameworks.Archive
private import codeql.ruby.frameworks.GraphQL
private import codeql.ruby.frameworks.Rails
private import codeql.ruby.frameworks.Stdlib

View File

@@ -0,0 +1,33 @@
/**
* Provides classes for working with archive libraries.
*/
private import ruby
private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.ApiGraphs
/**
* Classes and predicates for modeling the RubyZip library
*/
module RubyZip {
/**
* A call to `Zip::File.new`, considered as a `FileSystemAccess`
*/
class RubyZipFileNew extends DataFlow::CallNode, FileSystemAccess::Range {
RubyZipFileNew() { this = API::getTopLevelMember("Zip").getMember("File").getAnInstantiation() }
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
}
/**
* A call to `Zip::File.open`, considered as a `FileSystemAccess`.
*/
class RubyZipFileOpen extends DataFlow::CallNode, FileSystemAccess::Range {
RubyZipFileOpen() {
this = API::getTopLevelMember("Zip").getMember("File").getAMethodCall("open")
}
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
}
}

View File

@@ -0,0 +1,4 @@
rubyZipFileOpens
| Archive.rb:2:12:2:35 | call to open |
rubyZipFileNew
| Archive.rb:5:12:5:34 | call to new |

View File

@@ -0,0 +1,6 @@
private import ruby
private import codeql.ruby.frameworks.Archive
query predicate rubyZipFileOpens(RubyZip::RubyZipFileOpen f) { any() }
query predicate rubyZipFileNew(RubyZip::RubyZipFileNew f) { any() }

View File

@@ -0,0 +1,5 @@
# `foo_file` is a RubyZip `Zip::File.open` instance
foo_file = Zip::File.open(filename)
# `new_file` is a RubyZip `Zip::File.new` instance
new_file = Zip::File.new(filename)

View File

@@ -0,0 +1,82 @@
class TestContoller < ActionController::Base
# this is vulnerable
def upload
untar params[:file], params[:filename]
end
# this is vulnerable
def unpload_zip
unzip params[:file]
end
# this is vulnerable
def create_new_zip
zip params[:filename], files
end
# these are not vulnerable because of the string compare sanitizer
def safe_upload_string_compare
filename = params[:filename]
if filename == "safefile.tar"
untar params[:file], filename
end
end
def safe_upload_zip_string_compare
filename = params[:filename]
if filename == "safefile.zip"
unzip filename
end
end
# these are not vulnerable beacuse of the string array compare sanitizer
def safe_upload_string_array_compare
filename = params[:filename]
if ["safefile1.tar", "safefile2.tar"].include? filename
untar params[:file], filename
end
end
def safe_upload_zip_string_array_compare
filename = params[:filename]
if ["safefile1.zip", "safefile2.zip"].include? filename
unzip filename
end
end
# these are our two sinks
def untar(io, destination)
Gem::Package::TarReader.new io do |tar|
tar.each do |tarfile|
destination_file = File.join destination, tarfile.full_name
if tarfile.directory?
FileUtils.mkdir_p destination_file
else
destination_directory = File.dirname(destination_file)
FileUtils.mkdir_p destination_directory unless File.directory?(destination_directory)
File.open destination_file, "wb" do |f|
f.print tarfile.read
end
end
end
end
end
def unzip(file)
Zip::File.open(file) do |zip_file|
zip_file.each do |entry|
entry.extract
end
end
end
def zip(filename, files = [])
Zip::File.new(filename) do |zf|
files.each do |f|
zf.add f
end
end
end
end

View File

@@ -1,4 +1,15 @@
edges
| ArchiveApiPathTraversal.rb:5:26:5:31 | call to params : | ArchiveApiPathTraversal.rb:5:26:5:42 | ...[...] : |
| ArchiveApiPathTraversal.rb:5:26:5:42 | ...[...] : | ArchiveApiPathTraversal.rb:49:17:49:27 | destination : |
| ArchiveApiPathTraversal.rb:10:11:10:16 | call to params : | ArchiveApiPathTraversal.rb:10:11:10:23 | ...[...] : |
| ArchiveApiPathTraversal.rb:10:11:10:23 | ...[...] : | ArchiveApiPathTraversal.rb:67:13:67:16 | file : |
| ArchiveApiPathTraversal.rb:15:9:15:14 | call to params : | ArchiveApiPathTraversal.rb:15:9:15:25 | ...[...] : |
| ArchiveApiPathTraversal.rb:15:9:15:25 | ...[...] : | ArchiveApiPathTraversal.rb:75:11:75:18 | filename : |
| ArchiveApiPathTraversal.rb:49:17:49:27 | destination : | ArchiveApiPathTraversal.rb:52:38:52:48 | destination : |
| ArchiveApiPathTraversal.rb:52:28:52:67 | call to join : | ArchiveApiPathTraversal.rb:59:21:59:36 | destination_file |
| ArchiveApiPathTraversal.rb:52:38:52:48 | destination : | ArchiveApiPathTraversal.rb:52:28:52:67 | call to join : |
| ArchiveApiPathTraversal.rb:67:13:67:16 | file : | ArchiveApiPathTraversal.rb:68:20:68:23 | file |
| ArchiveApiPathTraversal.rb:75:11:75:18 | filename : | ArchiveApiPathTraversal.rb:76:19:76:26 | filename |
| tainted_path.rb:4:12:4:17 | call to params : | tainted_path.rb:4:12:4:24 | ...[...] : |
| tainted_path.rb:4:12:4:24 | ...[...] : | tainted_path.rb:5:26:5:29 | path |
| tainted_path.rb:10:12:10:43 | call to absolute_path : | tainted_path.rb:11:26:11:29 | path |
@@ -26,6 +37,20 @@ edges
| tainted_path.rb:59:40:59:45 | call to params : | tainted_path.rb:59:40:59:52 | ...[...] : |
| tainted_path.rb:59:40:59:52 | ...[...] : | tainted_path.rb:59:12:59:53 | call to new : |
nodes
| ArchiveApiPathTraversal.rb:5:26:5:31 | call to params : | semmle.label | call to params : |
| ArchiveApiPathTraversal.rb:5:26:5:42 | ...[...] : | semmle.label | ...[...] : |
| ArchiveApiPathTraversal.rb:10:11:10:16 | call to params : | semmle.label | call to params : |
| ArchiveApiPathTraversal.rb:10:11:10:23 | ...[...] : | semmle.label | ...[...] : |
| ArchiveApiPathTraversal.rb:15:9:15:14 | call to params : | semmle.label | call to params : |
| ArchiveApiPathTraversal.rb:15:9:15:25 | ...[...] : | semmle.label | ...[...] : |
| ArchiveApiPathTraversal.rb:49:17:49:27 | destination : | semmle.label | destination : |
| ArchiveApiPathTraversal.rb:52:28:52:67 | call to join : | semmle.label | call to join : |
| ArchiveApiPathTraversal.rb:52:38:52:48 | destination : | semmle.label | destination : |
| ArchiveApiPathTraversal.rb:59:21:59:36 | destination_file | semmle.label | destination_file |
| ArchiveApiPathTraversal.rb:67:13:67:16 | file : | semmle.label | file : |
| ArchiveApiPathTraversal.rb:68:20:68:23 | file | semmle.label | file |
| ArchiveApiPathTraversal.rb:75:11:75:18 | filename : | semmle.label | filename : |
| ArchiveApiPathTraversal.rb:76:19:76:26 | filename | semmle.label | filename |
| tainted_path.rb:4:12:4:17 | call to params : | semmle.label | call to params : |
| tainted_path.rb:4:12:4:24 | ...[...] : | semmle.label | ...[...] : |
| tainted_path.rb:5:26:5:29 | path | semmle.label | path |
@@ -63,6 +88,9 @@ nodes
| tainted_path.rb:60:26:60:29 | path | semmle.label | path |
subpaths
#select
| 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 $@. | ArchiveApiPathTraversal.rb:5:26:5:31 | call to params | a user-provided value |
| ArchiveApiPathTraversal.rb:68:20:68:23 | file | ArchiveApiPathTraversal.rb:10:11:10:16 | call to params : | ArchiveApiPathTraversal.rb:68:20:68:23 | file | This path depends on $@. | ArchiveApiPathTraversal.rb:10:11:10:16 | call to params | a user-provided value |
| ArchiveApiPathTraversal.rb:76:19:76:26 | filename | ArchiveApiPathTraversal.rb:15:9:15:14 | call to params : | ArchiveApiPathTraversal.rb:76:19:76:26 | filename | This path depends on $@. | ArchiveApiPathTraversal.rb:15:9:15:14 | call to params | a user-provided value |
| tainted_path.rb:5:26:5:29 | path | tainted_path.rb:4:12:4:17 | call to params : | tainted_path.rb:5:26:5:29 | path | This path depends on $@. | tainted_path.rb:4:12:4:17 | call to params | a user-provided value |
| tainted_path.rb:11:26:11:29 | path | tainted_path.rb:10:31:10:36 | call to params : | tainted_path.rb:11:26:11:29 | path | This path depends on $@. | tainted_path.rb:10:31:10:36 | call to params | a user-provided value |
| tainted_path.rb:17:26:17:29 | path | tainted_path.rb:16:28:16:33 | call to params : | tainted_path.rb:17:26:17:29 | path | This path depends on $@. | tainted_path.rb:16:28:16:33 | call to params | a user-provided value |