-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ZipSlip and TarSlip query to ruby #12208
Conversation
QHelp previews: ruby/ql/src/experimental/cwe-022-zipslip/ZipSlip.qhelpArbitrary file write during zipfile/tarfile extractionExtracting files from a malicious tar archive without validating that the destination file path is within the destination directory can cause files outside the destination directory to be overwritten, due to the possible presence of directory traversal elements ( Tar archives contain archive entries representing each file in the archive. These entries include a file path for the entry, but these file paths are not restricted and may contain unexpected special elements such as the directory traversal element ( For example, if a tar archive contains a file entry RecommendationEnsure that output paths constructed from tar archive entries are validated to prevent writing files to unexpected locations. The recommended way of writing an output file from a tar archive entry is to check that ExampleIn this example an archive is extracted without validating file paths. If class FilesController < ActionController::Base
def zipFileUnsafe
path = params[:path]
Zip::File.open(path).each do |entry|
File.open(entry.name, "wb") do |os|
entry.read
end
end
end
def tarReaderUnsafe
path = params[:path]
file_stream = IO.new(IO.sysopen(path))
tarfile = Gem::Package::TarReader.new(file_stream)
tarfile.each do |entry|
::File.open(entry.full_name, "wb") do |os|
entry.read
end
end
end
end To fix this vulnerability, we need to check that the path does not contain any class FilesController < ActionController::Base
def zipFileSafe
path = params[:path]
Zip::File.open(path).each do |entry|
entry_path = entry.name
next if !File.expand_path(entry_path).start_with?('/safepath/')
File.open(entry_path, "wb") do |os|
entry.read
end
end
end
def tarReaderSafe
path = params[:path]
file_stream = IO.new(IO.sysopen(path))
tarfile = Gem::Package::TarReader.new(file_stream)
tarfile.each do |entry|
entry_path = entry.full_name
raise ExtractFailed if entry_path != "/safepath"
::File.open(entry_path, "wb") do |os|
entry.read
end
end
end
end References
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this pull request. I see you used the Python version of this query as inspiration. There are a couple of Python related bits left in the documentation, let's fix that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some nitpicks, generally LGTM.
Since this is a new query for Ruby, our CI will want a change note. This can be added as a ruby/ql/src/change-notes/2023-02-17-zipslip-query.md
with contents like:
---
category: newQuery
---
* Added a new query, `rb/zip-slip`, to detect arbitrary file writes during extraction of zip/tar archives.
Thanks for those suggestions @alexrford and sorry @aibaars for leaving out python references. I think it's better now |
From https://github.com/github/codeql/actions/runs/4203950763/jobs/7293988883
Could you auto-format the files? You can run: |
/** | ||
* A call to `Zip::File.open(path)`, considered a flow source. | ||
*/ | ||
private class ZipFileOpen extends Source { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following project resulted in some strange looking results:
https://gist.github.com/aibaars/9f25acb1dcc81b4731d9781b955d3ddd
I think you need to model the case where the Zip::File.open
method is called with a block differently from the case where it is called without block; Zip::File.open(path) do |file| ... end
and file = Zip::File.open(path)
are different.
@gregxsunday Could you also move the query and its support files to the |
@gregxsunday These are the results of the query on the latest version of discourse: https://gist.github.com/aibaars/b1456bbab6c13ed6e3192819ad95632d The results look sensible, but I think the query is not picking up checks like the one in https://github.com/discourse/discourse/blob/1a653d2ce931b76b3b217d9e4f571bcef124d2a9/lib/compression/strategy.rb#LL21C35-L21C35 . If you can teach the query to handle this type of sanitizer that would be great. However, this is not a requirement for the query being accepted. |
Hi @aibaars, I will move it to Regarding handling blocks and the sanitizer, as I wrote in the original PR message, I am aware of those limitations and I had tried to solve them but I simply don't know how to. I'm happy to improve the query if you hint me into the good direction. |
Sorry about that, I had forgotten you already mentioned those in the description. Let's add handling blocks, but leave the sanitizer as future work. I think something like the following should work. We treat the return value as a source if there is no block, and if there is a block then we treat the block's first parameter as a source: ZipFileOpen() {
exists(API::MethodAccessNode zipOpen |
zipOpen = API::getTopLevelMember("Zip").getMember("File").getMethod("open") and
// If argument refers to a string object, then it's a hardcoded path and
// this file is safe.
not zipOpen.getParameter(0).asSource().getConstantValue().isStringlikeValue(_)
|
not exists(zipOpen.getBlock()) and this = zipOpen.getReturn().asSource()
or
this = zipOpen.getBlock().getParameter(0).asSource()
)
} |
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me, thanks!
You're right that there is a potential zip-slip issue in roo-rb/roo. The previous version sort of found the problem, but for the wrong reason. To find the result properly we need a model for https://github.com/roo-rb/roo/blob/3fecab545b943962213cdf5f0cbe2cdb142940b7/lib/roo/base.rb#L603 If we add a model for the @hmac Shall we add modelling the With models for the If we also model that |
@aibaars we do have |
@gregxsunday Thanks a lot for this contribution! |
This query detects previously undetected RCE in Discourse by uploading malicious ZIP/Tar archives.
CVE-2022-36066
Similarly as with Python's TarSlip query, I consider unarchiving a file as a source and file creation as a sink. I've modelled all 3 libraries used by Discourse (
Gem::Package::TarReader
,Zip::File
andZlib::GzipReader
).There are two things are I think should be improved here but I simply don't know how to do them as I'm new to CodeQL.
Flaw 1: Flow in blocks
The flow is followed correctly when the zipfile is returned from a function with a
yield
keyword but when it's inline like this, the flow breaks. I've tried debugging it with partial flow queries but I don't know how to model that.Flaw 2: Sanitizer
The way the bug was fixed in Discourse is by adding a function
And calling it with the untrusted path as the first argument. While my predicate
isSanitizer
can detect inlineFile.expand_path(path)
, it doesn't catch this case. I probably need to look at more than one node inisSanitizer
but I don't quite know how to do it.