Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,79 @@ module TarSlip {
}
}

/**
* A call to `shutil.unpack_archive`, considered as a flow source.
*
* The archive filename is not hardcoded, so it may come from user input.
*/
class ShutilUnpackArchiveSource extends Source {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove (should not be a source).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5084883ShutilUnpackArchiveSource removed.

ShutilUnpackArchiveSource() {
this = API::moduleImport("shutil").getMember("unpack_archive").getACall() and
not this.(DataFlow::CallCfgNode).getArg(0).getALocalSource().asExpr() instanceof StringLiteral
}
}

/**
* A call to `shutil.unpack_archive`, considered as a flow sink.
*
* The archive filename is not hardcoded, so it may come from user input.
*/
class ShutilUnpackArchiveSink extends Sink {
ShutilUnpackArchiveSink() {
this = API::moduleImport("shutil").getMember("unpack_archive").getACall() and
not this.(DataFlow::CallCfgNode).getArg(0).getALocalSource().asExpr() instanceof StringLiteral
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this line, not really needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5084883 — changed this to call.getArg(0) (the filename argument), which makes the literal check redundant and removed it.

}
}

/**
* Holds if `call` is a subprocess call that invokes `tar` for archive extraction
* with at least one non-literal argument (the archive filename).
*
* Detects patterns like `subprocess.run(["tar", "-xf", untrusted_filename])`.
*/
private predicate isSubprocessTarExtraction(DataFlow::CallCfgNode call) {
exists(SequenceNode cmdList |
call =
API::moduleImport("subprocess")
.getMember(["run", "call", "check_call", "check_output", "Popen"])
.getACall() and
cmdList = call.getArg(0).asCfgNode() and
// Command must be "tar" exactly or a path ending in "/tar" (e.g. "/usr/bin/tar")
exists(string cmd |
cmd = cmdList.getElement(0).getNode().(StringLiteral).getText() and
(cmd = "tar" or cmd.matches("%/tar"))
) and
// At least one extraction-related flag must be present:
// single-dash flags containing 'x' (like -x, -xf, -xvf) or the long option --extract
exists(string flag |
flag = cmdList.getElement(_).getNode().(StringLiteral).getText() and
(flag.regexpMatch("-[a-zA-Z]*x[a-zA-Z]*") or flag = "--extract")
) and
// At least one non-literal argument (the archive filename)
exists(int i |
i > 0 and
exists(cmdList.getElement(i)) and
not cmdList.getElement(i).getNode() instanceof StringLiteral
)
)
}

/**
* A call to `subprocess` functions that invokes `tar` for archive extraction,
* considered as a flow source.
*/
class SubprocessTarExtractionSource extends Source {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove (should not be a source).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5084883SubprocessTarExtractionSource removed.

SubprocessTarExtractionSource() { isSubprocessTarExtraction(this) }
}

/**
* A call to `subprocess` functions that invokes `tar` for archive extraction,
* considered as a flow sink.
*/
class SubprocessTarExtractionSink extends Sink {
SubprocessTarExtractionSink() { isSubprocessTarExtraction(this) }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should not be the call that is the sink, but instead the element in the command list that corresponds to the file to extract.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5084883 — the sink is now cmdList.getElement(i) (the specific non-literal element in the command list, i.e. the archive filename), not the call node.

}

/**
* Holds if `g` clears taint for `tarInfo`.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ nodes
| tarslip.py:112:1:112:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar |
| tarslip.py:112:7:112:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| tarslip.py:113:24:113:26 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar |
| tarslip.py:122:1:122:49 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| tarslip.py:126:1:126:51 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| tarslip.py:127:1:127:58 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
subpaths
#select
| tarslip.py:15:1:15:3 | ControlFlowNode for tar | tarslip.py:14:7:14:39 | ControlFlowNode for Attribute() | tarslip.py:15:1:15:3 | ControlFlowNode for tar | This file extraction depends on a $@. | tarslip.py:14:7:14:39 | ControlFlowNode for Attribute() | potentially untrusted source |
Expand All @@ -64,3 +67,6 @@ subpaths
| tarslip.py:96:17:96:21 | ControlFlowNode for entry | tarslip.py:94:7:94:39 | ControlFlowNode for Attribute() | tarslip.py:96:17:96:21 | ControlFlowNode for entry | This file extraction depends on a $@. | tarslip.py:94:7:94:39 | ControlFlowNode for Attribute() | potentially untrusted source |
| tarslip.py:110:1:110:3 | ControlFlowNode for tar | tarslip.py:109:7:109:39 | ControlFlowNode for Attribute() | tarslip.py:110:1:110:3 | ControlFlowNode for tar | This file extraction depends on a $@. | tarslip.py:109:7:109:39 | ControlFlowNode for Attribute() | potentially untrusted source |
| tarslip.py:113:24:113:26 | ControlFlowNode for tar | tarslip.py:112:7:112:39 | ControlFlowNode for Attribute() | tarslip.py:113:24:113:26 | ControlFlowNode for tar | This file extraction depends on a $@. | tarslip.py:112:7:112:39 | ControlFlowNode for Attribute() | potentially untrusted source |
| tarslip.py:122:1:122:49 | ControlFlowNode for Attribute() | tarslip.py:122:1:122:49 | ControlFlowNode for Attribute() | tarslip.py:122:1:122:49 | ControlFlowNode for Attribute() | This file extraction depends on a $@. | tarslip.py:122:1:122:49 | ControlFlowNode for Attribute() | potentially untrusted source |
| tarslip.py:126:1:126:51 | ControlFlowNode for Attribute() | tarslip.py:126:1:126:51 | ControlFlowNode for Attribute() | tarslip.py:126:1:126:51 | ControlFlowNode for Attribute() | This file extraction depends on a $@. | tarslip.py:126:1:126:51 | ControlFlowNode for Attribute() | potentially untrusted source |
| tarslip.py:127:1:127:58 | ControlFlowNode for Attribute() | tarslip.py:127:1:127:58 | ControlFlowNode for Attribute() | tarslip.py:127:1:127:58 | ControlFlowNode for Attribute() | This file extraction depends on a $@. | tarslip.py:127:1:127:58 | ControlFlowNode for Attribute() | potentially untrusted source |
13 changes: 13 additions & 0 deletions python/ql/test/query-tests/Security/CWE-022-TarSlip/tarslip.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,16 @@ def safemembers(members):

tar = tarfile.open(unsafe_filename_tar)
tar.extractall(members=safemembers(tar), filter=extraction_filter) # safe -- we assume `safemembers` makes up for the unsafe filter

import shutil
import subprocess

# shutil.unpack_archive
shutil.unpack_archive(unsafe_filename_tar, "out") # unsafe
shutil.unpack_archive("safe.tar", "out") # safe

# subprocess tar extraction
subprocess.run(["tar", "-xf", unsafe_filename_tar]) # unsafe
subprocess.check_call(["tar", "-xf", unsafe_filename_tar]) # unsafe
subprocess.run(["tar", "-xf", "safe.tar"]) # safe
subprocess.run(["echo", unsafe_filename_tar]) # safe - not a tar extraction
Loading