зеркало из https://github.com/github/codeql.git
Merge pull request #8004 from ahmed-farid-dev/ZipSlip
Add query to detect ZipSlip
This commit is contained in:
Коммит
626770aaab
|
@ -0,0 +1,56 @@
|
||||||
|
<!DOCTYPE qhelp PUBLIC
|
||||||
|
"-//Semmle//qhelp//EN"
|
||||||
|
"qhelp.dtd">
|
||||||
|
<qhelp>
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
<p>Extracting files from a malicious zip 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 (<code>..</code>) in
|
||||||
|
archive paths.</p>
|
||||||
|
|
||||||
|
<p>Zip 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 (<code>..</code>). If these
|
||||||
|
file paths are used to determine an output file to write the contents of the archive item to, then
|
||||||
|
the file may be written to an unexpected location. This can result in sensitive information being
|
||||||
|
revealed or deleted, or an attacker being able to influence behavior by modifying unexpected
|
||||||
|
files.</p>
|
||||||
|
|
||||||
|
<p>For example, if a Zip archive contains a file entry <code>..\sneaky-file</code>, and the Zip archive
|
||||||
|
is extracted to the directory <code>c:\output</code>, then naively combining the paths would result
|
||||||
|
in an output file path of <code>c:\output\..\sneaky-file</code>, which would cause the file to be
|
||||||
|
written to <code>c:\sneaky-file</code>.</p>
|
||||||
|
|
||||||
|
</overview>
|
||||||
|
<recommendation>
|
||||||
|
|
||||||
|
<p>Ensure that output paths constructed from Zip archive entries are validated
|
||||||
|
to prevent writing files to unexpected locations.</p>
|
||||||
|
|
||||||
|
<p>The recommended way of writing an output file from a Zip archive entry is to call <code>extract()</code> or <code>extractall()</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</recommendation>
|
||||||
|
|
||||||
|
<example>
|
||||||
|
<p>
|
||||||
|
In this example an archive is extracted without validating file paths.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<sample src="zipslip_bad.py" />
|
||||||
|
|
||||||
|
<p>To fix this vulnerability, we need to call the function <code>extractall()</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<sample src="zipslip_good.py" />
|
||||||
|
|
||||||
|
</example>
|
||||||
|
<references>
|
||||||
|
<li>
|
||||||
|
Snyk:
|
||||||
|
<a href="https://snyk.io/research/zip-slip-vulnerability">Zip Slip Vulnerability</a>.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</references>
|
||||||
|
</qhelp>
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* @name Arbitrary file write during archive extraction ("Zip Slip")
|
||||||
|
* @description Extracting files from a malicious archive without validating that the
|
||||||
|
* destination file path is within the destination directory can cause files outside
|
||||||
|
* the destination directory to be overwritten.
|
||||||
|
* @kind path-problem
|
||||||
|
* @id py/zipslip
|
||||||
|
* @problem.severity error
|
||||||
|
* @security-severity 7.5
|
||||||
|
* @precision high
|
||||||
|
* @tags security
|
||||||
|
* external/cwe/cwe-022
|
||||||
|
*/
|
||||||
|
|
||||||
|
import python
|
||||||
|
import experimental.semmle.python.security.ZipSlip
|
||||||
|
import DataFlow::PathGraph
|
||||||
|
|
||||||
|
from ZipSlipConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||||
|
where config.hasFlowPath(source, sink)
|
||||||
|
select sink.getNode(), source, sink, "Extraction of zipfile from $@", source.getNode(),
|
||||||
|
"a potentially untrusted source"
|
|
@ -0,0 +1,16 @@
|
||||||
|
import zipfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
def unzip(filename):
|
||||||
|
with tarfile.open(filename) as zipf:
|
||||||
|
#BAD : This could write any file on the filesystem.
|
||||||
|
for entry in zipf:
|
||||||
|
shutil.copyfile(entry, "/tmp/unpack/")
|
||||||
|
|
||||||
|
def unzip4(filename):
|
||||||
|
zf = zipfile.ZipFile(filename)
|
||||||
|
filelist = zf.namelist()
|
||||||
|
for x in filelist:
|
||||||
|
with zf.open(x) as srcf:
|
||||||
|
shutil.copyfileobj(srcf, dstfile)
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
def unzip(filename, dir):
|
||||||
|
zf = zipfile.ZipFile(filename)
|
||||||
|
zf.extractall(dir)
|
||||||
|
|
||||||
|
|
||||||
|
def unzip1(filename, dir):
|
||||||
|
zf = zipfile.ZipFile(filename)
|
||||||
|
zf.extract(dir)
|
|
@ -14,6 +14,73 @@ private import semmle.python.dataflow.new.RemoteFlowSources
|
||||||
private import semmle.python.dataflow.new.TaintTracking
|
private import semmle.python.dataflow.new.TaintTracking
|
||||||
private import experimental.semmle.python.Frameworks
|
private import experimental.semmle.python.Frameworks
|
||||||
|
|
||||||
|
/** Provides classes for modeling copying file related APIs. */
|
||||||
|
module CopyFile {
|
||||||
|
/**
|
||||||
|
* A data flow node for copying file.
|
||||||
|
*
|
||||||
|
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||||
|
* extend `CopyFile` instead.
|
||||||
|
*/
|
||||||
|
abstract class Range extends DataFlow::Node {
|
||||||
|
/**
|
||||||
|
* Gets the argument containing the path.
|
||||||
|
*/
|
||||||
|
abstract DataFlow::Node getAPathArgument();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets fsrc argument.
|
||||||
|
*/
|
||||||
|
abstract DataFlow::Node getfsrcArgument();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data flow node for copying file.
|
||||||
|
*
|
||||||
|
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||||
|
* extend `CopyFile::Range` instead.
|
||||||
|
*/
|
||||||
|
class CopyFile extends DataFlow::Node {
|
||||||
|
CopyFile::Range range;
|
||||||
|
|
||||||
|
CopyFile() { this = range }
|
||||||
|
|
||||||
|
DataFlow::Node getAPathArgument() { result = range.getAPathArgument() }
|
||||||
|
|
||||||
|
DataFlow::Node getfsrcArgument() { result = range.getfsrcArgument() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provides classes for modeling log related APIs. */
|
||||||
|
module LogOutput {
|
||||||
|
/**
|
||||||
|
* A data flow node for log output.
|
||||||
|
*
|
||||||
|
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||||
|
* extend `LogOutput` instead.
|
||||||
|
*/
|
||||||
|
abstract class Range extends DataFlow::Node {
|
||||||
|
/**
|
||||||
|
* Get the parameter value of the log output function.
|
||||||
|
*/
|
||||||
|
abstract DataFlow::Node getAnInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data flow node for log output.
|
||||||
|
*
|
||||||
|
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||||
|
* extend `LogOutput::Range` instead.
|
||||||
|
*/
|
||||||
|
class LogOutput extends DataFlow::Node {
|
||||||
|
LogOutput::Range range;
|
||||||
|
|
||||||
|
LogOutput() { this = range }
|
||||||
|
|
||||||
|
DataFlow::Node getAnInput() { result = range.getAnInput() }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since there is both XML module in normal and experimental Concepts,
|
* Since there is both XML module in normal and experimental Concepts,
|
||||||
* we have to rename the experimental module as this.
|
* we have to rename the experimental module as this.
|
||||||
|
|
|
@ -14,3 +14,4 @@ private import experimental.semmle.python.libraries.PyJWT
|
||||||
private import experimental.semmle.python.libraries.Python_JWT
|
private import experimental.semmle.python.libraries.Python_JWT
|
||||||
private import experimental.semmle.python.libraries.Authlib
|
private import experimental.semmle.python.libraries.Authlib
|
||||||
private import experimental.semmle.python.libraries.PythonJose
|
private import experimental.semmle.python.libraries.PythonJose
|
||||||
|
private import experimental.semmle.python.frameworks.CopyFile
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
private import python
|
||||||
|
private import experimental.semmle.python.Concepts
|
||||||
|
private import semmle.python.dataflow.new.DataFlow
|
||||||
|
private import semmle.python.ApiGraphs
|
||||||
|
|
||||||
|
private module CopyFileImpl {
|
||||||
|
/**
|
||||||
|
* The `shutil` module provides methods to copy or move files.
|
||||||
|
* See:
|
||||||
|
* - https://docs.python.org/3/library/shutil.html#shutil.copyfile
|
||||||
|
* - https://docs.python.org/3/library/shutil.html#shutil.copy
|
||||||
|
* - https://docs.python.org/3/library/shutil.html#shutil.copy2
|
||||||
|
* - https://docs.python.org/3/library/shutil.html#shutil.copytree
|
||||||
|
* - https://docs.python.org/3/library/shutil.html#shutil.move
|
||||||
|
*/
|
||||||
|
private class CopyFiles extends DataFlow::CallCfgNode, CopyFile::Range {
|
||||||
|
CopyFiles() {
|
||||||
|
this =
|
||||||
|
API::moduleImport("shutil")
|
||||||
|
.getMember(["copyfile", "copy", "copy2", "copytree", "move"])
|
||||||
|
.getACall()
|
||||||
|
}
|
||||||
|
|
||||||
|
override DataFlow::Node getAPathArgument() {
|
||||||
|
result in [this.getArg(0), this.getArgByName("src")]
|
||||||
|
}
|
||||||
|
|
||||||
|
override DataFlow::Node getfsrcArgument() { none() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: once we have flow summaries, model `shutil.copyfileobj` which copies the content between its' file-like arguments.
|
||||||
|
// See https://docs.python.org/3/library/shutil.html#shutil.copyfileobj
|
||||||
|
private class CopyFileobj extends DataFlow::CallCfgNode, CopyFile::Range {
|
||||||
|
CopyFileobj() { this = API::moduleImport("shutil").getMember("copyfileobj").getACall() }
|
||||||
|
|
||||||
|
override DataFlow::Node getfsrcArgument() {
|
||||||
|
result in [this.getArg(0), this.getArgByName("fsrc")]
|
||||||
|
}
|
||||||
|
|
||||||
|
override DataFlow::Node getAPathArgument() { none() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import python
|
||||||
|
import experimental.semmle.python.Concepts
|
||||||
|
import semmle.python.dataflow.new.DataFlow
|
||||||
|
import semmle.python.ApiGraphs
|
||||||
|
import semmle.python.dataflow.new.TaintTracking
|
||||||
|
|
||||||
|
class ZipSlipConfig extends TaintTracking::Configuration {
|
||||||
|
ZipSlipConfig() { this = "ZipSlipConfig" }
|
||||||
|
|
||||||
|
override predicate isSource(DataFlow::Node source) {
|
||||||
|
(
|
||||||
|
source =
|
||||||
|
API::moduleImport("zipfile").getMember("ZipFile").getReturn().getMember("open").getACall() or
|
||||||
|
source =
|
||||||
|
API::moduleImport("zipfile")
|
||||||
|
.getMember("ZipFile")
|
||||||
|
.getReturn()
|
||||||
|
.getMember("namelist")
|
||||||
|
.getACall() or
|
||||||
|
source = API::moduleImport("tarfile").getMember("open").getACall() or
|
||||||
|
source = API::moduleImport("tarfile").getMember("TarFile").getACall() or
|
||||||
|
source = API::moduleImport("bz2").getMember("open").getACall() or
|
||||||
|
source = API::moduleImport("bz2").getMember("BZ2File").getACall() or
|
||||||
|
source = API::moduleImport("gzip").getMember("GzipFile").getACall() or
|
||||||
|
source = API::moduleImport("gzip").getMember("open").getACall() or
|
||||||
|
source = API::moduleImport("lzma").getMember("open").getACall() or
|
||||||
|
source = API::moduleImport("lzma").getMember("LZMAFile").getACall()
|
||||||
|
) and
|
||||||
|
not source.getScope().getLocation().getFile().inStdlib()
|
||||||
|
}
|
||||||
|
|
||||||
|
override predicate isSink(DataFlow::Node sink) {
|
||||||
|
(
|
||||||
|
sink = any(CopyFile copyfile).getAPathArgument() or
|
||||||
|
sink = any(CopyFile copyfile).getfsrcArgument()
|
||||||
|
) and
|
||||||
|
not sink.getScope().getLocation().getFile().inStdlib()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
edges
|
||||||
|
| zipslip_bad.py:8:10:8:31 | ControlFlowNode for Attribute() | zipslip_bad.py:10:13:10:17 | SSA variable entry |
|
||||||
|
| zipslip_bad.py:10:13:10:17 | SSA variable entry | zipslip_bad.py:11:25:11:29 | ControlFlowNode for entry |
|
||||||
|
| zipslip_bad.py:14:10:14:28 | ControlFlowNode for Attribute() | zipslip_bad.py:16:13:16:17 | SSA variable entry |
|
||||||
|
| zipslip_bad.py:16:13:16:17 | SSA variable entry | zipslip_bad.py:17:26:17:30 | ControlFlowNode for entry |
|
||||||
|
| zipslip_bad.py:20:10:20:27 | ControlFlowNode for Attribute() | zipslip_bad.py:22:13:22:17 | SSA variable entry |
|
||||||
|
| zipslip_bad.py:22:13:22:17 | SSA variable entry | zipslip_bad.py:23:29:23:33 | ControlFlowNode for entry |
|
||||||
|
| zipslip_bad.py:27:10:27:22 | ControlFlowNode for Attribute() | zipslip_bad.py:29:13:29:13 | SSA variable x |
|
||||||
|
| zipslip_bad.py:29:13:29:13 | SSA variable x | zipslip_bad.py:30:25:30:25 | ControlFlowNode for x |
|
||||||
|
| zipslip_bad.py:34:16:34:28 | ControlFlowNode for Attribute() | zipslip_bad.py:35:9:35:9 | SSA variable x |
|
||||||
|
| zipslip_bad.py:35:9:35:9 | SSA variable x | zipslip_bad.py:37:32:37:32 | ControlFlowNode for x |
|
||||||
|
nodes
|
||||||
|
| zipslip_bad.py:8:10:8:31 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||||
|
| zipslip_bad.py:10:13:10:17 | SSA variable entry | semmle.label | SSA variable entry |
|
||||||
|
| zipslip_bad.py:11:25:11:29 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry |
|
||||||
|
| zipslip_bad.py:14:10:14:28 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||||
|
| zipslip_bad.py:16:13:16:17 | SSA variable entry | semmle.label | SSA variable entry |
|
||||||
|
| zipslip_bad.py:17:26:17:30 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry |
|
||||||
|
| zipslip_bad.py:20:10:20:27 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||||
|
| zipslip_bad.py:22:13:22:17 | SSA variable entry | semmle.label | SSA variable entry |
|
||||||
|
| zipslip_bad.py:23:29:23:33 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry |
|
||||||
|
| zipslip_bad.py:27:10:27:22 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||||
|
| zipslip_bad.py:29:13:29:13 | SSA variable x | semmle.label | SSA variable x |
|
||||||
|
| zipslip_bad.py:30:25:30:25 | ControlFlowNode for x | semmle.label | ControlFlowNode for x |
|
||||||
|
| zipslip_bad.py:34:16:34:28 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||||
|
| zipslip_bad.py:35:9:35:9 | SSA variable x | semmle.label | SSA variable x |
|
||||||
|
| zipslip_bad.py:37:32:37:32 | ControlFlowNode for x | semmle.label | ControlFlowNode for x |
|
||||||
|
subpaths
|
||||||
|
#select
|
||||||
|
| zipslip_bad.py:11:25:11:29 | ControlFlowNode for entry | zipslip_bad.py:8:10:8:31 | ControlFlowNode for Attribute() | zipslip_bad.py:11:25:11:29 | ControlFlowNode for entry | Extraction of zipfile from $@ | zipslip_bad.py:8:10:8:31 | ControlFlowNode for Attribute() | a potentially untrusted source |
|
||||||
|
| zipslip_bad.py:17:26:17:30 | ControlFlowNode for entry | zipslip_bad.py:14:10:14:28 | ControlFlowNode for Attribute() | zipslip_bad.py:17:26:17:30 | ControlFlowNode for entry | Extraction of zipfile from $@ | zipslip_bad.py:14:10:14:28 | ControlFlowNode for Attribute() | a potentially untrusted source |
|
||||||
|
| zipslip_bad.py:23:29:23:33 | ControlFlowNode for entry | zipslip_bad.py:20:10:20:27 | ControlFlowNode for Attribute() | zipslip_bad.py:23:29:23:33 | ControlFlowNode for entry | Extraction of zipfile from $@ | zipslip_bad.py:20:10:20:27 | ControlFlowNode for Attribute() | a potentially untrusted source |
|
||||||
|
| zipslip_bad.py:30:25:30:25 | ControlFlowNode for x | zipslip_bad.py:27:10:27:22 | ControlFlowNode for Attribute() | zipslip_bad.py:30:25:30:25 | ControlFlowNode for x | Extraction of zipfile from $@ | zipslip_bad.py:27:10:27:22 | ControlFlowNode for Attribute() | a potentially untrusted source |
|
||||||
|
| zipslip_bad.py:37:32:37:32 | ControlFlowNode for x | zipslip_bad.py:34:16:34:28 | ControlFlowNode for Attribute() | zipslip_bad.py:37:32:37:32 | ControlFlowNode for x | Extraction of zipfile from $@ | zipslip_bad.py:34:16:34:28 | ControlFlowNode for Attribute() | a potentially untrusted source |
|
|
@ -0,0 +1 @@
|
||||||
|
experimental/Security/CWE-022/ZipSlip.ql
|
|
@ -0,0 +1,39 @@
|
||||||
|
import tarfile
|
||||||
|
import shutil
|
||||||
|
import bz2
|
||||||
|
import gzip
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
def unzip(filename):
|
||||||
|
with tarfile.open(filename) as zipf:
|
||||||
|
#BAD : This could write any file on the filesystem.
|
||||||
|
for entry in zipf:
|
||||||
|
shutil.move(entry, "/tmp/unpack/")
|
||||||
|
|
||||||
|
def unzip1(filename):
|
||||||
|
with gzip.open(filename) as zipf:
|
||||||
|
#BAD : This could write any file on the filesystem.
|
||||||
|
for entry in zipf:
|
||||||
|
shutil.copy2(entry, "/tmp/unpack/")
|
||||||
|
|
||||||
|
def unzip2(filename):
|
||||||
|
with bz2.open(filename) as zipf:
|
||||||
|
#BAD : This could write any file on the filesystem.
|
||||||
|
for entry in zipf:
|
||||||
|
shutil.copyfile(entry, "/tmp/unpack/")
|
||||||
|
|
||||||
|
def unzip3(filename):
|
||||||
|
zf = zipfile.ZipFile(filename)
|
||||||
|
with zf.namelist() as filelist:
|
||||||
|
#BAD : This could write any file on the filesystem.
|
||||||
|
for x in filelist:
|
||||||
|
shutil.copy(x, "/tmp/unpack/")
|
||||||
|
|
||||||
|
def unzip4(filename):
|
||||||
|
zf = zipfile.ZipFile(filename)
|
||||||
|
filelist = zf.namelist()
|
||||||
|
for x in filelist:
|
||||||
|
with zf.open(x) as srcf:
|
||||||
|
shutil.copyfileobj(x, "/tmp/unpack/")
|
||||||
|
|
||||||
|
import tty # to set the import root so we can identify the standard library
|
|
@ -0,0 +1,14 @@
|
||||||
|
import zipfile
|
||||||
|
import tarfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
def unzip(filename, dir):
|
||||||
|
zf = zipfile.ZipFile(filename)
|
||||||
|
zf.extractall(dir)
|
||||||
|
|
||||||
|
|
||||||
|
def unzip1(filename, dir):
|
||||||
|
zf = zipfile.ZipFile(filename)
|
||||||
|
zf.extract(dir)
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче