diff --git a/python/ql/src/experimental/Security/CWE-022bis/TarSlip.qhelp b/python/ql/src/experimental/Security/CWE-022bis/TarSlip.qhelp new file mode 100644 index 000000000000..75ba15abf429 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-022bis/TarSlip.qhelp @@ -0,0 +1,60 @@ + + + + +

Extracting files from a malicious tarball 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 (..) in +archive path names.

+ +

Tarball 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 (..). 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.

+ +

For example, if a tarball contains a file entry ../sneaky-file, and the tarball +is extracted to the directory /tmp/tmp123, then naively combining the paths would result +in an output file path of /tmp/tmp123/../sneaky-file, which would cause the file to be +written to /tmp/.

+ +
+ + +

Ensure that output paths constructed from tarball entries are validated +to prevent writing files to unexpected locations.

+ +

The recommended way of writing an output file from a tarball entry is to call extract() or extractall(). +

+ +
+ + +

+In this example an archive is extracted without validating file paths. +

+ + + +

To fix this vulnerability, we need to call the function extractall(). +

+ + + +
+ +
  • +Snyk: +Zip Slip Vulnerability. +
  • + +
  • + Tarfile documentation + extractall() warning +
  • +
    +
    diff --git a/python/ql/src/experimental/Security/CWE-022bis/TarSlipImprov.ql b/python/ql/src/experimental/Security/CWE-022bis/TarSlipImprov.ql new file mode 100755 index 000000000000..e614c1a3d78a --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-022bis/TarSlipImprov.ql @@ -0,0 +1,122 @@ +/** + * @name Arbitrary file write during tarfile extraction + * @description Extracting 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. + * @kind path-problem + * @id py/tarslip + * @problem.severity error + * @security-severity 7.5 + * @precision high + * @tags security + * external/cwe/cwe-022 + */ + +import python +import semmle.python.dataflow.new.DataFlow +import semmle.python.dataflow.new.TaintTracking +import DataFlow::PathGraph +import semmle.python.ApiGraphs +import semmle.python.dataflow.new.internal.Attributes +import semmle.python.dataflow.new.BarrierGuards +import semmle.python.dataflow.new.RemoteFlowSources + +/** + * Handle those three cases of Tarfile opens: + * - `tarfile.open()` + * - `tarfile.TarFile()` + * - `MKtarfile.Tarfile.open()` + */ +API::Node tarfileOpen() { + result in [ + API::moduleImport("tarfile").getMember(["open", "TarFile"]), + API::moduleImport("tarfile").getMember("TarFile").getASubclass().getMember("open") + ] +} + +/** + * Handle the previous three cases, plus the use of `closing` in the previous cases + */ +class AllTarfileOpens extends API::CallNode { + AllTarfileOpens() { + this = tarfileOpen().getACall() + or + exists(API::Node closing, Node arg | + closing = API::moduleImport("contextlib").getMember("closing") and + this = closing.getACall() and + arg = this.getArg(0) and + arg = tarfileOpen().getACall() + ) + } +} + +/** + * A taint-tracking configuration for detecting more "TarSlip" vulnerabilities. + */ +class Configuration extends TaintTracking::Configuration { + Configuration() { this = "TarSlip" } + + override predicate isSource(DataFlow::Node source) { source = tarfileOpen().getACall() } + + override predicate isSink(DataFlow::Node sink) { + ( + // A sink capturing method calls to `extractall` without `members` argument. + // For a call to `file.extractall` without `members` argument, `file` is considered a sink. + exists(MethodCallNode call, AllTarfileOpens atfo | + call = atfo.getReturn().getMember("extractall").getACall() and + not exists(Node arg | arg = call.getArgByName("members")) and + sink = call.getObject() + ) + or + // A sink capturing method calls to `extractall` with `members` argument. + // For a call to `file.extractall` with `members` argument, `file` is considered a sink if not + // a the `members` argument contains a NameConstant as None, a List or call to the method `getmembers`. + // Otherwise, the argument of `members` is considered a sink. + exists(MethodCallNode call, Node arg, AllTarfileOpens atfo | + call = atfo.getReturn().getMember("extractall").getACall() and + arg = call.getArgByName("members") and + if + arg.asCfgNode() instanceof NameConstantNode or + arg.asCfgNode() instanceof ListNode + then sink = call.getObject() + else + if arg.(MethodCallNode).getMethodName() = "getmembers" + then sink = arg.(MethodCallNode).getObject() + else sink = call.getArgByName("members") + ) + or + // An argument to `extract` is considered a sink. + exists(AllTarfileOpens atfo | + sink = atfo.getReturn().getMember("extract").getACall().getArg(0) + ) + or + //An argument to `_extract_member` is considered a sink. + exists(MethodCallNode call, AllTarfileOpens atfo | + call = atfo.getReturn().getMember("_extract_member").getACall() and + call.getArg(1).(AttrRead).accesses(sink, "name") + ) + ) and + not sink.getScope().getLocation().getFile().inStdlib() + } + + override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { + exists(AttrRead attr, MethodCallNode call | + attr.accesses(nodeFrom, "getmembers") and + nodeFrom = call.getObject() and + nodeFrom instanceof AllTarfileOpens and + nodeTo = call + ) + or + exists(API::CallNode closing | + closing = API::moduleImport("contextlib").getMember("closing").getACall() and + nodeFrom = closing.getArg(0) and + nodeFrom = tarfileOpen().getReturn().getAValueReachingSink() and + nodeTo = closing + ) + } +} + +from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink +where config.hasFlowPath(source, sink) +select sink, source, sink, "Extraction of tarfile from $@ to a potentially untrusted source $@.", + source.getNode(), source.getNode().toString(), sink.getNode(), sink.getNode().toString() diff --git a/python/ql/test/experimental/query-tests/Security/CWE-022/TarSlip.expected b/python/ql/test/experimental/query-tests/Security/CWE-022/TarSlip.expected new file mode 100644 index 000000000000..774b0005433e --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-022/TarSlip.expected @@ -0,0 +1,143 @@ +edges +| TarSlipImprov.py:15:7:15:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:17:5:17:10 | GSSA Variable member | +| TarSlipImprov.py:17:5:17:10 | GSSA Variable member | TarSlipImprov.py:22:35:22:40 | ControlFlowNode for result | +| TarSlipImprov.py:26:21:26:27 | ControlFlowNode for tarfile | TarSlipImprov.py:28:9:28:14 | SSA variable member | +| TarSlipImprov.py:28:9:28:14 | SSA variable member | TarSlipImprov.py:36:12:36:17 | ControlFlowNode for result | +| TarSlipImprov.py:38:7:38:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:39:65:39:67 | ControlFlowNode for tar | +| TarSlipImprov.py:39:65:39:67 | ControlFlowNode for tar | TarSlipImprov.py:26:21:26:27 | ControlFlowNode for tarfile | +| TarSlipImprov.py:39:65:39:67 | ControlFlowNode for tar | TarSlipImprov.py:39:49:39:68 | ControlFlowNode for members_filter1() | +| TarSlipImprov.py:43:6:43:38 | ControlFlowNode for Attribute() | TarSlipImprov.py:44:9:44:13 | GSSA Variable entry | +| TarSlipImprov.py:44:9:44:13 | GSSA Variable entry | TarSlipImprov.py:47:21:47:25 | ControlFlowNode for entry | +| TarSlipImprov.py:54:6:54:38 | ControlFlowNode for Attribute() | TarSlipImprov.py:56:9:56:13 | GSSA Variable entry | +| TarSlipImprov.py:56:9:56:13 | GSSA Variable entry | TarSlipImprov.py:58:21:58:25 | ControlFlowNode for entry | +| TarSlipImprov.py:88:6:88:43 | ControlFlowNode for Attribute() | TarSlipImprov.py:91:5:91:7 | ControlFlowNode for tar | +| TarSlipImprov.py:111:7:111:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:115:9:115:11 | ControlFlowNode for tar | +| TarSlipImprov.py:123:6:123:29 | ControlFlowNode for Attribute() | TarSlipImprov.py:124:9:124:13 | GSSA Variable entry | +| TarSlipImprov.py:124:9:124:13 | GSSA Variable entry | TarSlipImprov.py:125:36:125:40 | ControlFlowNode for entry | +| TarSlipImprov.py:129:6:129:26 | ControlFlowNode for Attribute() | TarSlipImprov.py:130:5:130:7 | ControlFlowNode for tar | +| TarSlipImprov.py:133:7:133:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:134:1:134:3 | ControlFlowNode for tar | +| TarSlipImprov.py:141:6:141:29 | ControlFlowNode for Attribute() | TarSlipImprov.py:142:9:142:13 | GSSA Variable entry | +| TarSlipImprov.py:142:9:142:13 | GSSA Variable entry | TarSlipImprov.py:143:36:143:40 | ControlFlowNode for entry | +| TarSlipImprov.py:176:6:176:31 | ControlFlowNode for Attribute() | TarSlipImprov.py:177:9:177:13 | GSSA Variable entry | +| TarSlipImprov.py:177:9:177:13 | GSSA Variable entry | TarSlipImprov.py:178:36:178:40 | ControlFlowNode for entry | +| TarSlipImprov.py:182:6:182:31 | ControlFlowNode for Attribute() | TarSlipImprov.py:183:9:183:13 | GSSA Variable entry | +| TarSlipImprov.py:183:9:183:13 | GSSA Variable entry | TarSlipImprov.py:184:21:184:25 | ControlFlowNode for entry | +| TarSlipImprov.py:188:7:188:27 | ControlFlowNode for Attribute() | TarSlipImprov.py:189:1:189:3 | ControlFlowNode for tar | +| TarSlipImprov.py:193:6:193:31 | ControlFlowNode for Attribute() | TarSlipImprov.py:194:49:194:51 | ControlFlowNode for tar | +| TarSlipImprov.py:210:6:210:43 | ControlFlowNode for Attribute() | TarSlipImprov.py:211:5:211:7 | ControlFlowNode for tar | +| TarSlipImprov.py:231:6:231:38 | ControlFlowNode for Attribute() | TarSlipImprov.py:233:9:233:9 | GSSA Variable f | +| TarSlipImprov.py:233:9:233:9 | GSSA Variable f | TarSlipImprov.py:236:44:236:50 | ControlFlowNode for members | +| TarSlipImprov.py:258:6:258:26 | ControlFlowNode for Attribute() | TarSlipImprov.py:259:9:259:13 | GSSA Variable entry | +| TarSlipImprov.py:259:9:259:13 | GSSA Variable entry | TarSlipImprov.py:261:25:261:29 | ControlFlowNode for entry | +| TarSlipImprov.py:264:6:264:38 | ControlFlowNode for Attribute() | TarSlipImprov.py:265:9:265:13 | GSSA Variable entry | +| TarSlipImprov.py:265:9:265:13 | GSSA Variable entry | TarSlipImprov.py:268:21:268:25 | ControlFlowNode for entry | +| TarSlipImprov.py:271:6:271:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:272:9:272:13 | GSSA Variable entry | +| TarSlipImprov.py:272:9:272:13 | GSSA Variable entry | TarSlipImprov.py:274:25:274:29 | ControlFlowNode for entry | +| TarSlipImprov.py:276:6:276:38 | ControlFlowNode for Attribute() | TarSlipImprov.py:277:9:277:13 | GSSA Variable entry | +| TarSlipImprov.py:277:9:277:13 | GSSA Variable entry | TarSlipImprov.py:280:21:280:25 | ControlFlowNode for entry | +| TarSlipImprov.py:283:6:283:51 | ControlFlowNode for Attribute() | TarSlipImprov.py:284:5:284:7 | ControlFlowNode for tar | +| TarSlipImprov.py:287:7:287:28 | ControlFlowNode for Attribute() | TarSlipImprov.py:288:49:288:51 | ControlFlowNode for tar | +| TarSlipImprov.py:292:7:292:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:293:1:293:3 | ControlFlowNode for tar | +| TarSlipImprov.py:300:6:300:51 | ControlFlowNode for Attribute() | TarSlipImprov.py:301:49:301:51 | ControlFlowNode for tar | +| TarSlipImprov.py:304:7:304:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:306:5:306:10 | GSSA Variable member | +| TarSlipImprov.py:306:5:306:10 | GSSA Variable member | TarSlipImprov.py:310:49:310:54 | ControlFlowNode for result | +nodes +| TarSlipImprov.py:15:7:15:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:17:5:17:10 | GSSA Variable member | semmle.label | GSSA Variable member | +| TarSlipImprov.py:22:35:22:40 | ControlFlowNode for result | semmle.label | ControlFlowNode for result | +| TarSlipImprov.py:26:21:26:27 | ControlFlowNode for tarfile | semmle.label | ControlFlowNode for tarfile | +| TarSlipImprov.py:28:9:28:14 | SSA variable member | semmle.label | SSA variable member | +| TarSlipImprov.py:36:12:36:17 | ControlFlowNode for result | semmle.label | ControlFlowNode for result | +| TarSlipImprov.py:38:7:38:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:39:49:39:68 | ControlFlowNode for members_filter1() | semmle.label | ControlFlowNode for members_filter1() | +| TarSlipImprov.py:39:65:39:67 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:43:6:43:38 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:44:9:44:13 | GSSA Variable entry | semmle.label | GSSA Variable entry | +| TarSlipImprov.py:47:21:47:25 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | +| TarSlipImprov.py:54:6:54:38 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:56:9:56:13 | GSSA Variable entry | semmle.label | GSSA Variable entry | +| TarSlipImprov.py:58:21:58:25 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | +| TarSlipImprov.py:88:6:88:43 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:91:5:91:7 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:111:7:111:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:115:9:115:11 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:123:6:123:29 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:124:9:124:13 | GSSA Variable entry | semmle.label | GSSA Variable entry | +| TarSlipImprov.py:125:36:125:40 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | +| TarSlipImprov.py:129:6:129:26 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:130:5:130:7 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:133:7:133:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:134:1:134:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:141:6:141:29 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:142:9:142:13 | GSSA Variable entry | semmle.label | GSSA Variable entry | +| TarSlipImprov.py:143:36:143:40 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | +| TarSlipImprov.py:176:6:176:31 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:177:9:177:13 | GSSA Variable entry | semmle.label | GSSA Variable entry | +| TarSlipImprov.py:178:36:178:40 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | +| TarSlipImprov.py:182:6:182:31 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:183:9:183:13 | GSSA Variable entry | semmle.label | GSSA Variable entry | +| TarSlipImprov.py:184:21:184:25 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | +| TarSlipImprov.py:188:7:188:27 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:189:1:189:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:193:6:193:31 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:194:49:194:51 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:210:6:210:43 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:211:5:211:7 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:231:6:231:38 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:233:9:233:9 | GSSA Variable f | semmle.label | GSSA Variable f | +| TarSlipImprov.py:236:44:236:50 | ControlFlowNode for members | semmle.label | ControlFlowNode for members | +| TarSlipImprov.py:254:1:254:31 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:258:6:258:26 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:259:9:259:13 | GSSA Variable entry | semmle.label | GSSA Variable entry | +| TarSlipImprov.py:261:25:261:29 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | +| TarSlipImprov.py:264:6:264:38 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:265:9:265:13 | GSSA Variable entry | semmle.label | GSSA Variable entry | +| TarSlipImprov.py:268:21:268:25 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | +| TarSlipImprov.py:271:6:271:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:272:9:272:13 | GSSA Variable entry | semmle.label | GSSA Variable entry | +| TarSlipImprov.py:274:25:274:29 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | +| TarSlipImprov.py:276:6:276:38 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:277:9:277:13 | GSSA Variable entry | semmle.label | GSSA Variable entry | +| TarSlipImprov.py:280:21:280:25 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | +| TarSlipImprov.py:283:6:283:51 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:284:5:284:7 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:287:7:287:28 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:288:49:288:51 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:292:7:292:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:293:1:293:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:300:6:300:51 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:301:49:301:51 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| TarSlipImprov.py:304:7:304:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| TarSlipImprov.py:306:5:306:10 | GSSA Variable member | semmle.label | GSSA Variable member | +| TarSlipImprov.py:310:49:310:54 | ControlFlowNode for result | semmle.label | ControlFlowNode for result | +| TarSlipImprov.py:316:1:316:46 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +subpaths +| TarSlipImprov.py:39:65:39:67 | ControlFlowNode for tar | TarSlipImprov.py:26:21:26:27 | ControlFlowNode for tarfile | TarSlipImprov.py:36:12:36:17 | ControlFlowNode for result | TarSlipImprov.py:39:49:39:68 | ControlFlowNode for members_filter1() | +#select +| TarSlipImprov.py:22:35:22:40 | ControlFlowNode for result | TarSlipImprov.py:15:7:15:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:22:35:22:40 | ControlFlowNode for result | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:15:7:15:39 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:22:35:22:40 | ControlFlowNode for result | ControlFlowNode for result | +| TarSlipImprov.py:39:49:39:68 | ControlFlowNode for members_filter1() | TarSlipImprov.py:38:7:38:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:39:49:39:68 | ControlFlowNode for members_filter1() | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:38:7:38:39 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:39:49:39:68 | ControlFlowNode for members_filter1() | ControlFlowNode for members_filter1() | +| TarSlipImprov.py:47:21:47:25 | ControlFlowNode for entry | TarSlipImprov.py:43:6:43:38 | ControlFlowNode for Attribute() | TarSlipImprov.py:47:21:47:25 | ControlFlowNode for entry | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:43:6:43:38 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:47:21:47:25 | ControlFlowNode for entry | ControlFlowNode for entry | +| TarSlipImprov.py:58:21:58:25 | ControlFlowNode for entry | TarSlipImprov.py:54:6:54:38 | ControlFlowNode for Attribute() | TarSlipImprov.py:58:21:58:25 | ControlFlowNode for entry | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:54:6:54:38 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:58:21:58:25 | ControlFlowNode for entry | ControlFlowNode for entry | +| TarSlipImprov.py:91:5:91:7 | ControlFlowNode for tar | TarSlipImprov.py:88:6:88:43 | ControlFlowNode for Attribute() | TarSlipImprov.py:91:5:91:7 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:88:6:88:43 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:91:5:91:7 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:115:9:115:11 | ControlFlowNode for tar | TarSlipImprov.py:111:7:111:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:115:9:115:11 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:111:7:111:39 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:115:9:115:11 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:125:36:125:40 | ControlFlowNode for entry | TarSlipImprov.py:123:6:123:29 | ControlFlowNode for Attribute() | TarSlipImprov.py:125:36:125:40 | ControlFlowNode for entry | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:123:6:123:29 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:125:36:125:40 | ControlFlowNode for entry | ControlFlowNode for entry | +| TarSlipImprov.py:130:5:130:7 | ControlFlowNode for tar | TarSlipImprov.py:129:6:129:26 | ControlFlowNode for Attribute() | TarSlipImprov.py:130:5:130:7 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:129:6:129:26 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:130:5:130:7 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:134:1:134:3 | ControlFlowNode for tar | TarSlipImprov.py:133:7:133:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:134:1:134:3 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:133:7:133:39 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:134:1:134:3 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:143:36:143:40 | ControlFlowNode for entry | TarSlipImprov.py:141:6:141:29 | ControlFlowNode for Attribute() | TarSlipImprov.py:143:36:143:40 | ControlFlowNode for entry | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:141:6:141:29 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:143:36:143:40 | ControlFlowNode for entry | ControlFlowNode for entry | +| TarSlipImprov.py:178:36:178:40 | ControlFlowNode for entry | TarSlipImprov.py:176:6:176:31 | ControlFlowNode for Attribute() | TarSlipImprov.py:178:36:178:40 | ControlFlowNode for entry | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:176:6:176:31 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:178:36:178:40 | ControlFlowNode for entry | ControlFlowNode for entry | +| TarSlipImprov.py:184:21:184:25 | ControlFlowNode for entry | TarSlipImprov.py:182:6:182:31 | ControlFlowNode for Attribute() | TarSlipImprov.py:184:21:184:25 | ControlFlowNode for entry | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:182:6:182:31 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:184:21:184:25 | ControlFlowNode for entry | ControlFlowNode for entry | +| TarSlipImprov.py:189:1:189:3 | ControlFlowNode for tar | TarSlipImprov.py:188:7:188:27 | ControlFlowNode for Attribute() | TarSlipImprov.py:189:1:189:3 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:188:7:188:27 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:189:1:189:3 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:194:49:194:51 | ControlFlowNode for tar | TarSlipImprov.py:193:6:193:31 | ControlFlowNode for Attribute() | TarSlipImprov.py:194:49:194:51 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:193:6:193:31 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:194:49:194:51 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:211:5:211:7 | ControlFlowNode for tar | TarSlipImprov.py:210:6:210:43 | ControlFlowNode for Attribute() | TarSlipImprov.py:211:5:211:7 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:210:6:210:43 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:211:5:211:7 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:236:44:236:50 | ControlFlowNode for members | TarSlipImprov.py:231:6:231:38 | ControlFlowNode for Attribute() | TarSlipImprov.py:236:44:236:50 | ControlFlowNode for members | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:231:6:231:38 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:236:44:236:50 | ControlFlowNode for members | ControlFlowNode for members | +| TarSlipImprov.py:254:1:254:31 | ControlFlowNode for Attribute() | TarSlipImprov.py:254:1:254:31 | ControlFlowNode for Attribute() | TarSlipImprov.py:254:1:254:31 | ControlFlowNode for Attribute() | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:254:1:254:31 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:254:1:254:31 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | +| TarSlipImprov.py:261:25:261:29 | ControlFlowNode for entry | TarSlipImprov.py:258:6:258:26 | ControlFlowNode for Attribute() | TarSlipImprov.py:261:25:261:29 | ControlFlowNode for entry | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:258:6:258:26 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:261:25:261:29 | ControlFlowNode for entry | ControlFlowNode for entry | +| TarSlipImprov.py:268:21:268:25 | ControlFlowNode for entry | TarSlipImprov.py:264:6:264:38 | ControlFlowNode for Attribute() | TarSlipImprov.py:268:21:268:25 | ControlFlowNode for entry | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:264:6:264:38 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:268:21:268:25 | ControlFlowNode for entry | ControlFlowNode for entry | +| TarSlipImprov.py:274:25:274:29 | ControlFlowNode for entry | TarSlipImprov.py:271:6:271:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:274:25:274:29 | ControlFlowNode for entry | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:271:6:271:39 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:274:25:274:29 | ControlFlowNode for entry | ControlFlowNode for entry | +| TarSlipImprov.py:280:21:280:25 | ControlFlowNode for entry | TarSlipImprov.py:276:6:276:38 | ControlFlowNode for Attribute() | TarSlipImprov.py:280:21:280:25 | ControlFlowNode for entry | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:276:6:276:38 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:280:21:280:25 | ControlFlowNode for entry | ControlFlowNode for entry | +| TarSlipImprov.py:284:5:284:7 | ControlFlowNode for tar | TarSlipImprov.py:283:6:283:51 | ControlFlowNode for Attribute() | TarSlipImprov.py:284:5:284:7 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:283:6:283:51 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:284:5:284:7 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:288:49:288:51 | ControlFlowNode for tar | TarSlipImprov.py:287:7:287:28 | ControlFlowNode for Attribute() | TarSlipImprov.py:288:49:288:51 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:287:7:287:28 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:288:49:288:51 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:293:1:293:3 | ControlFlowNode for tar | TarSlipImprov.py:292:7:292:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:293:1:293:3 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:292:7:292:39 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:293:1:293:3 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:301:49:301:51 | ControlFlowNode for tar | TarSlipImprov.py:300:6:300:51 | ControlFlowNode for Attribute() | TarSlipImprov.py:301:49:301:51 | ControlFlowNode for tar | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:300:6:300:51 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:301:49:301:51 | ControlFlowNode for tar | ControlFlowNode for tar | +| TarSlipImprov.py:310:49:310:54 | ControlFlowNode for result | TarSlipImprov.py:304:7:304:39 | ControlFlowNode for Attribute() | TarSlipImprov.py:310:49:310:54 | ControlFlowNode for result | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:304:7:304:39 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:310:49:310:54 | ControlFlowNode for result | ControlFlowNode for result | +| TarSlipImprov.py:316:1:316:46 | ControlFlowNode for Attribute() | TarSlipImprov.py:316:1:316:46 | ControlFlowNode for Attribute() | TarSlipImprov.py:316:1:316:46 | ControlFlowNode for Attribute() | Extraction of tarfile from $@ to a potentially untrusted source $@. | TarSlipImprov.py:316:1:316:46 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | TarSlipImprov.py:316:1:316:46 | ControlFlowNode for Attribute() | ControlFlowNode for Attribute() | diff --git a/python/ql/test/experimental/query-tests/Security/CWE-022/TarSlip.qlref b/python/ql/test/experimental/query-tests/Security/CWE-022/TarSlip.qlref new file mode 100644 index 000000000000..a518196b6982 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-022/TarSlip.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-022bis/TarSlipImprov.ql diff --git a/python/ql/test/experimental/query-tests/Security/CWE-022/TarSlipImprov.py b/python/ql/test/experimental/query-tests/Security/CWE-022/TarSlipImprov.py new file mode 100644 index 000000000000..15bc66b4aea5 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-022/TarSlipImprov.py @@ -0,0 +1,316 @@ +#!/usr/bin/python + +import sys +import tarfile +import tempfile +import os +from tarfile import TarFile +from contextlib import closing, contextmanager +import subprocess +import os.path + +unsafe_filename_tar = sys.argv[2] +safe_filename_tar = "safe_path.tar" + +tar = tarfile.open(unsafe_filename_tar) +result = [] +for member in tar: + if ".." in member.name: + raise ValueError("Path in member name !!!") + result.append(member) +path = unsafe_filename_tar +tar.extractall(path=path, members=result) +tar.close() + + +def members_filter1(tarfile): + result = [] + for member in tarfile: + if '../' in member.name: + print('Member name container directory traversal sequence') + continue + elif member.issym() or member.islnk(): + print('Symlink to external resource') + continue + result.append(member) + return result + +tar = tarfile.open(unsafe_filename_tar) +tar.extractall(path=tempfile.mkdtemp(), members=members_filter1(tar)) +tar.close() + + +with tarfile.open(unsafe_filename_tar) as tar: + for entry in tar: + if ".." in entry.name: + raise ValueError("Illegal tar archive entry") + tar.extract(entry, "/tmp/unpack/") + + +def _validate_archive_name(name, target): + if not os.path.abspath(os.path.join(target, name)).startswith(target + os.path.sep): + raise ValueError(f"Provided language pack contains invalid name {name}") + +with tarfile.open(unsafe_filename_tar) as tar: + target = "/tmp/unpack" + for entry in tar: + _validate_archive_name(entry.name, target) + tar.extract(entry, target) + + +def members_filter2(tarfile): + result = [] + for member in tarfile.getmembers(): + if '../' in member.name: + print('Member name container directory traversal sequence') + continue + elif (member.issym() or member.islnk()) and ('../' in member.linkname): + print('Symlink to external resource') + continue + result.append(member) + return result + +tar = tarfile.open(unsafe_filename_tar) +tar.extractall(path=tempfile.mkdtemp(), members=members_filter2(tar)) +tar.close() + + +def _validate_tar_info(info, target): + _validate_archive_name(info.name, target) + if not (info.isfile() or info.isdir()): + raise ValueError("Provided language pack contains invalid file type") + +def _validate_archive_name(name, target): + if not os.path.abspath(os.path.join(target, name)).startswith(target + os.path.sep): + raise ValueError(f"Provided language pack contains invalid name {name}") + +target = "/tmp/unpack" +with tarfile.open(unsafe_filename_tar, "r") as tar: + for info in tar.getmembers(): + _validate_tar_info(info, target) + tar.extractall(target) + + +def members_filter3(tarfile): + result = [] + for member in tarfile.getmembers(): + if '../' in member.name: + print('Member name container directory traversal sequence') + continue + elif member.issym() or member.islnk(): + print('Symlink to external resource') + continue + result.append(member) + return result + +tar = tarfile.open(unsafe_filename_tar) +tar.extractall(path=tempfile.mkdtemp(), members=members_filter3(tar)) +tar.close() + + +tar = tarfile.open(unsafe_filename_tar) +tarf = tar.getmembers() +for f in tarf: + if not f.issym(): + tar.extractall(path=tempfile.mkdtemp(), members=[f]) +tar.close() + + +class MKTar(TarFile): + pass + +tarball = unsafe_filename_tar +with MKTar.open(name=tarball) as tar: + for entry in tar: + tar._extract_member(entry, entry.name) + + +tarball = unsafe_filename_tar +with tarfile.open(tarball) as tar: + tar.extractall() + + +tar = tarfile.open(unsafe_filename_tar) +tar.extractall(path=tempfile.mkdtemp(), members=None) + + +class MKTar(tarfile.TarFile): + pass + +tarball = unsafe_filename_tar +with MKTar.open(name=tarball) as tar: + for entry in tar: + tar._extract_member(entry, entry.name) + + +@contextmanager +def py2_tarxz(filename): + with tempfile.TemporaryFile() as tmp: + subprocess.check_call(["xz", "-dc", filename], stdout=tmp.fileno()) + tmp.seek(0) + with closing(tarfile.TarFile(fileobj=tmp)) as tf: + yield tf + +def unpack_tarball(tar_filename, dest): + if sys.version_info[0] < 3 and tar_filename.endswith('.xz'): + # Py 2.7 lacks lzma support + tar_cm = py2_tarxz(tar_filename) + else: + tar_cm = closing(tarfile.open(tar_filename)) + + base_dir = None + with tar_cm as tarc: + for member in tarc: + base_name = member.name.split('/')[0] + if base_dir is None: + base_dir = base_name + elif base_dir != base_name: + print('Unexpected path in %s: %s' % (tar_filename, base_name)) + tarc.extractall(dest) + return os.path.join(dest, base_dir) + +unpack_tarball(unsafe_filename_tar, "/tmp/unpack") + + +tarball = unsafe_filename_tar +with tarfile.open(name=tarball) as tar: + for entry in tar: + tar._extract_member(entry, entry.name) + + +tarball = unsafe_filename_tar +with tarfile.open(name=tarball) as tar: + for entry in tar: + tar.extract(entry, "/tmp/unpack/") + + +tarball = unsafe_filename_tar +tar = tarfile.open(tarball) +tar.extractall("/tmp/unpack/") + + +tarball = unsafe_filename_tar +with tarfile.open(tarball, "r") as tar: + tar.extractall(path="/tmp/unpack/", members=tar) + + +def members_filter4(tarfile): + result = [] + for member in tarfile.getmembers(): + if member.issym() or member.islnk(): + print('Symlink to external resource') + continue + result.append(member) + return result +tar = tarfile.open(unsafe_filename_tar) +tar.extractall(path=tempfile.mkdtemp(), members=members_filter4(tar)) +tar.close() + + +with tarfile.open(unsafe_filename_tar, "r") as tar: + tar.extractall(path="/tmp/unpack") + + +def members_filter5(tarfile): + result = [] + for member in tarfile.getmembers(): + if member.issym(): + print('Symlink to external resource') + continue + result.append(member) + return result +tar = tarfile.open(unsafe_filename_tar) +tar.extractall(path=tempfile.mkdtemp(), members=members_filter5(tar)) +tar.close() + + +filename = unsafe_filename_tar +tmp_dir = "/tmp/" + +read_type = "r:gz" if filename.endswith("tgz") else "r" +with tarfile.open(filename, read_type) as corpus_tar: + members = [] + for f in corpus_tar: + if not os.path.isfile(os.path.join(tmp_dir, f.name)): + members.append(f) + corpus_tar.extractall(tmp_dir, members=members) + + +def members_filter6(tarfile): + result = [] + for member in tarfile.getmembers(): + if not member.isreg(): + print('Symlink to external resource') + continue + result.append(member) + return result +tar = tarfile.open(filename) +tar.extractall(path=tempfile.mkdtemp(), members=members_filter6(tar)) +tar.close() + + +archive_path = unsafe_filename_tar +target_dir = "/tmp/unpack" +tarfile.open(archive_path, "r").extractall(path=target_dir) + + +tarball = unsafe_filename_tar +with tarfile.open(tarball) as tar: + for entry in tar: + if entry.isfile(): + tar.extract(entry, "/tmp/unpack/") + + +with tarfile.open(unsafe_filename_tar) as tar: + for entry in tar: + if entry.name.startswith("/"): + raise ValueError("Illegal tar archive entry") + tar.extract(entry, "/tmp/unpack/") + +tarball = unsafe_filename_tar +with tarfile.TarFile(tarball, mode="r") as tar: + for entry in tar: + if entry.isfile(): + tar.extract(entry, "/tmp/unpack/") + +with tarfile.open(unsafe_filename_tar) as tar: + for entry in tar: + if os.path.isabs(entry.name): + raise ValueError("Illegal tar archive entry") + tar.extract(entry, "/tmp/unpack/") + + +with tarfile.TarFile(unsafe_filename_tar, mode="r") as tar: + tar.extractall(path="/tmp/unpack") + + +tar = tarfile.open(filename) +tar.extractall(path=tempfile.mkdtemp(), members=tar.getmembers()) +tar.close() + + +tar = tarfile.open(unsafe_filename_tar) +tar.extractall(path=tempfile.mkdtemp(), members=None) + + +tar.extractall(path=tempfile.mkdtemp(), members=members_filter4(tar)) +tar.close() + + +with tarfile.TarFile(unsafe_filename_tar, mode="r") as tar: + tar.extractall(path="/tmp/unpack/", members=tar) + + +tar = tarfile.open(unsafe_filename_tar) +result = [] +for member in tar: + if member.issym(): + raise ValueError("But it is a symlink") + result.append(member) +tar.extractall(path=tempfile.mkdtemp(), members=result) +tar.close() + + +archive_path = unsafe_filename_tar +target_dir = "/tmp/unpack" +tarfile.TarFile(unsafe_filename_tar, mode="r").extractall(path=target_dir) \ No newline at end of file