mirror of
https://github.com/github/codeql.git
synced 2026-01-28 13:53:10 +01:00
147 lines
5.1 KiB
Plaintext
147 lines
5.1 KiB
Plaintext
/**
|
|
* Provides default sources, sinks and sanitizers for reasoning about
|
|
* unsafe zip and tar archive extraction, as well as extension points
|
|
* for adding your own.
|
|
*/
|
|
|
|
import javascript
|
|
|
|
module ZipSlip {
|
|
import TaintedPathCustomizations::TaintedPath as TaintedPath
|
|
|
|
/**
|
|
* A data flow source for unsafe archive extraction.
|
|
*/
|
|
abstract class Source extends DataFlow::Node {
|
|
/** Gets a flow label denoting the type of value for which this is a source. */
|
|
TaintedPath::Label::PosixPath getAFlowLabel() { result.isRelative() }
|
|
}
|
|
|
|
/**
|
|
* A data flow sink for unsafe archive extraction.
|
|
*/
|
|
abstract class Sink extends DataFlow::Node {
|
|
/** Gets a flow label denoting the type of value for which this is a sink. */
|
|
TaintedPath::Label::PosixPath getAFlowLabel() { any() }
|
|
}
|
|
|
|
/**
|
|
* Gets a node that can be a parsed archive.
|
|
*/
|
|
private DataFlow::SourceNode parsedArchive() {
|
|
result = DataFlow::moduleImport("unzipper").getAMemberCall("Parse")
|
|
or
|
|
result = DataFlow::moduleImport("unzip").getAMemberCall("Parse")
|
|
or
|
|
result = DataFlow::moduleImport("tar-stream").getAMemberCall("extract")
|
|
or
|
|
// `streamProducer.pipe(unzip.Parse())` is a typical (but not
|
|
// universal) pattern when using nodejs streams, whose return
|
|
// value is the parsed stream.
|
|
exists(DataFlow::MethodCallNode pipe |
|
|
pipe = result and
|
|
pipe.getMethodName() = "pipe" and
|
|
parsedArchive().flowsTo(pipe.getArgument(0))
|
|
)
|
|
}
|
|
|
|
/** Gets a property that is used to get a filename part of an archive entry. */
|
|
private string getAFilenameProperty() {
|
|
result = "path" // Used by library 'unzip'.
|
|
or
|
|
result = "name" // Used by library 'tar-stream'.
|
|
or
|
|
result = "linkname" // linked file name, used by 'tar-stream'.
|
|
}
|
|
|
|
/** An archive entry path access, as a source for unsafe archive extraction. */
|
|
class UnzipEntrySource extends Source {
|
|
// For example, in
|
|
// ```javascript
|
|
// const unzip = require('unzip');
|
|
//
|
|
// fs.createReadStream('archive.zip')
|
|
// .pipe(unzip.Parse())
|
|
// .on('entry', entry => {
|
|
// const path = entry.path;
|
|
// });
|
|
// ```
|
|
// there is an `UnzipEntrySource` node corresponding to
|
|
// the expression `entry.path`.
|
|
UnzipEntrySource() {
|
|
exists(DataFlow::CallNode cn |
|
|
cn = parsedArchive().getAMemberCall(EventEmitter::on()) and
|
|
cn.getArgument(0).mayHaveStringValue("entry") and
|
|
this = cn.getCallback(1).getParameter(0).getAPropertyRead(getAFilenameProperty())
|
|
)
|
|
}
|
|
}
|
|
|
|
/** An archive entry path access using the `adm-zip` package. */
|
|
class AdmZipEntrySource extends Source {
|
|
AdmZipEntrySource() {
|
|
exists(DataFlow::SourceNode admZip, DataFlow::SourceNode entry |
|
|
admZip = DataFlow::moduleImport("adm-zip").getAnInstantiation() and
|
|
this = entry.getAPropertyRead("entryName")
|
|
|
|
|
entry = admZip.getAMethodCall("getEntry")
|
|
or
|
|
exists(DataFlow::SourceNode entries | entries = admZip.getAMethodCall("getEntries") |
|
|
entry = entries.getAPropertyRead()
|
|
or
|
|
exists(string map | map = "map" or map = "forEach" |
|
|
entry = entries.getAMethodCall(map).getCallback(0).getParameter(0)
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private import semmle.javascript.DynamicPropertyAccess as DynamicPropertyAccess
|
|
|
|
/** A object key in the JSZip files object */
|
|
class JSZipFilesSource extends Source instanceof DynamicPropertyAccess::EnumeratedPropName {
|
|
JSZipFilesSource() {
|
|
super.getSourceObject() =
|
|
API::moduleImport("jszip").getInstance().getMember("files").getAnImmediateUse()
|
|
}
|
|
}
|
|
|
|
/** A relative path from iterating the files in the JSZip object */
|
|
class JSZipFileSource extends Source {
|
|
JSZipFileSource() {
|
|
this =
|
|
API::moduleImport("jszip")
|
|
.getInstance()
|
|
.getMember(["forEach", "filter"])
|
|
.getParameter(0)
|
|
.getParameter(0)
|
|
.getAnImmediateUse()
|
|
}
|
|
}
|
|
|
|
/** A call to `fs.createWriteStream`, as a sink for unsafe archive extraction. */
|
|
class CreateWriteStreamSink extends Sink {
|
|
CreateWriteStreamSink() {
|
|
// This is not covered by `FileSystemWriteSink`, because it is
|
|
// required that a write actually takes place to the stream.
|
|
// However, we want to consider even the bare `createWriteStream`
|
|
// to be a zipslip vulnerability since it may truncate an
|
|
// existing file.
|
|
this = NodeJSLib::FS::moduleMember("createWriteStream").getACall().getArgument(0)
|
|
or
|
|
// Not covered by `FileSystemWriteSink` because a later call
|
|
// to `fs.write` is required for a write to take place.
|
|
exists(DataFlow::CallNode call | this = call.getArgument(0) |
|
|
call = NodeJSLib::FS::moduleMember(["open", "openSync"]).getACall() and
|
|
call.getArgument(1).getStringValue().regexpMatch("(?i)w.{0,2}")
|
|
)
|
|
}
|
|
}
|
|
|
|
/** A file path of a file write, as a sink for unsafe archive extraction. */
|
|
class FileSystemWriteSink extends Sink {
|
|
FileSystemWriteSink() { exists(FileSystemWriteAccess fsw | fsw.getAPathArgument() = this) }
|
|
}
|
|
}
|