-
Notifications
You must be signed in to change notification settings - Fork 1.9k
JavaScript: Add new query for ZipSlip (CWE-022). #889
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
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
baa4f08
JS: Add new query for ZipSlip (CWE-022)
jcreedcmu abd2644
JS: Address review comments
jcreedcmu 32d48ba
JS: Run auto-formatter
jcreedcmu 23d37c7
JS: Unbreak TaintedPath
jcreedcmu b0636dd
JS: Better local flow through `.pipe` chaining
jcreedcmu 09b9a57
JS: More efficient reasoning through pipe
jcreedcmu 2fc2a39
JS: Address review comments
jcreedcmu caebdd2
JS: Fix incorrect sample link
jcreedcmu 674d279
JS: Address review comments
jcreedcmu c5e57da
JS: Actually use fileName in examples
jcreedcmu c1b218a
JS: Documentation fixes
jcreedcmu 86bbb5f
JS: Add ZipSlip query to security suite
jcreedcmu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| <!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 file contains a file entry <code>..\sneaky-file</code>, and the zip file | ||
| 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 check that | ||
| <code>".."</code> does not occur in the path. | ||
| </p> | ||
|
|
||
| </recommendation> | ||
|
|
||
| <example> | ||
| <p> | ||
| In this example an archive is extracted without validating file paths. | ||
| If <code>archive.zip</code> contained relative paths (for | ||
| instance, if it were created by something like <code>zip archive.zip | ||
| ../file.txt</code>) then executing this code could write to locations | ||
| outside the destination directory. | ||
| </p> | ||
|
|
||
| <sample src="ZipSlipBad.js" /> | ||
This conversation was marked as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| <p>To fix this vulnerability, we need to check that the path does not | ||
| contain any <code>".."</code> elements in it. | ||
| </p> | ||
|
|
||
| <sample src="ZipSlipGood.js" /> | ||
|
|
||
| </example> | ||
| <references> | ||
|
|
||
| <li> | ||
| Snyk: | ||
| <a href="https://snyk.io/research/zip-slip-vulnerability">Zip Slip Vulnerability</a>. | ||
| </li> | ||
| <li> | ||
| OWASP: | ||
| <a href="https://www.owasp.org/index.php/Path_traversal">Path Traversal</a>. | ||
| </li> | ||
|
|
||
| </references> | ||
| </qhelp> | ||
felicitymay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /** | ||
| * @name Arbitrary file write during zip extraction ("Zip Slip") | ||
| * @description 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. | ||
| * @kind path-problem | ||
| * @id js/zipslip | ||
| * @problem.severity error | ||
| * @precision medium | ||
| * @tags security | ||
| * external/cwe/cwe-022 | ||
| */ | ||
|
|
||
| import javascript | ||
| import semmle.javascript.security.dataflow.ZipSlip::ZipSlip | ||
| import DataFlow::PathGraph | ||
|
|
||
| from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink | ||
| where cfg.hasFlowPath(source, sink) | ||
| select sink.getNode(), source, sink, | ||
| "Unsanitized zip archive $@, which may contain '..', is used in a file system operation.", | ||
| source.getNode(), "item path" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| const fs = require('fs'); | ||
| const unzip = require('unzip'); | ||
|
|
||
| fs.createReadStream('archive.zip') | ||
| .pipe(unzip.Parse()) | ||
| .on('entry', entry => { | ||
| const fileName = entry.path; | ||
xiemaisi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // BAD: This could write any file on the filesystem. | ||
| entry.pipe(fs.createWriteStream(fileName)); | ||
felicitymay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| const fs = require('fs'); | ||
| const unzip = require('unzip'); | ||
|
|
||
| fs.createReadStream('archive.zip') | ||
| .pipe(unzip.Parse()) | ||
| .on('entry', entry => { | ||
| const fileName = entry.path; | ||
| // GOOD: ensures the path is safe to write to. | ||
| if (fileName.indexOf('..') == -1) { | ||
| entry.pipe(fs.createWriteStream(fileName)); | ||
| } | ||
| else { | ||
| console.log('skipping bad path', fileName); | ||
felicitymay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| }); | ||
111 changes: 111 additions & 0 deletions
111
javascript/ql/src/semmle/javascript/security/dataflow/ZipSlip.qll
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| /** | ||
| * Provides a taint tracking configuration for reasoning about unsafe zip extraction. | ||
| */ | ||
|
|
||
| import javascript | ||
|
|
||
| module ZipSlip { | ||
| /** | ||
| * A data flow source for unsafe zip extraction. | ||
| */ | ||
| abstract class Source extends DataFlow::Node { } | ||
|
|
||
| /** | ||
| * A data flow sink for unsafe zip extraction. | ||
| */ | ||
| abstract class Sink extends DataFlow::Node { } | ||
|
|
||
| /** | ||
| * A sanitizer guard for unsafe zip extraction. | ||
| */ | ||
| abstract class SanitizerGuard extends TaintTracking::SanitizerGuardNode, DataFlow::ValueNode { } | ||
|
|
||
| /** A taint tracking configuration for unsafe zip extraction. */ | ||
| class Configuration extends TaintTracking::Configuration { | ||
| Configuration() { this = "ZipSlip" } | ||
|
|
||
| override predicate isSource(DataFlow::Node source) { source instanceof Source } | ||
|
|
||
| override predicate isSink(DataFlow::Node sink) { sink instanceof Sink } | ||
|
|
||
| override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode nd) { | ||
| nd instanceof SanitizerGuard | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Gets a node that can be a parsed zip archive. | ||
| */ | ||
| private DataFlow::SourceNode parsedArchive() { | ||
| result = DataFlow::moduleImport("unzip").getAMemberCall("Parse") | ||
| 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)) | ||
| ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This disjunct doesn't seem to constrain
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, fixed. |
||
| } | ||
|
|
||
| /** A zip archive entry path access, as a source for unsafe zip 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("on") and | ||
| cn.getArgument(0).mayHaveStringValue("entry") and | ||
| this = cn.getCallback(1) | ||
| .getParameter(0) | ||
| .getAPropertyRead("path")) | ||
| } | ||
| } | ||
|
|
||
| /** A call to `fs.createWriteStream`, as a sink for unsafe zip 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 = DataFlow::moduleImport("fs").getAMemberCall("createWriteStream").getArgument(0) | ||
| } | ||
| } | ||
|
|
||
| /** A file path of a file write, as a sink for unsafe zip extraction. */ | ||
| class FileSystemWriteSink extends Sink { | ||
| FileSystemWriteSink() { exists(FileSystemWriteAccess fsw | fsw.getAPathArgument() = this) } | ||
| } | ||
|
|
||
| /** | ||
| * Gets a string which is sufficient to exclude to make | ||
| * a filepath definitely not refer to parent directories. | ||
xiemaisi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| */ | ||
| private string getAParentDirName() { result = ".." or result = "../" } | ||
|
|
||
| /** A check that a path string does not include '..' */ | ||
| class NoParentDirSanitizerGuard extends SanitizerGuard { | ||
| StringOps::Includes incl; | ||
|
|
||
| NoParentDirSanitizerGuard() { this = incl } | ||
|
|
||
| override predicate sanitizes(boolean outcome, Expr e) { | ||
| incl.getPolarity().booleanNot() = outcome and | ||
| incl.getBaseString().asExpr() = e and | ||
| incl.getSubstring().mayHaveStringValue(getAParentDirName()) | ||
| } | ||
| } | ||
| } | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
17 changes: 17 additions & 0 deletions
17
javascript/ql/test/query-tests/Security/CWE-022/ZipSlip/ZipSlip.expected
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| nodes | ||
| | ZipSlipBad2.js:5:9:5:46 | fileName | | ||
| | ZipSlipBad2.js:5:20:5:46 | 'output ... ry.path | | ||
| | ZipSlipBad2.js:5:37:5:46 | entry.path | | ||
| | ZipSlipBad2.js:6:22:6:29 | fileName | | ||
| | ZipSlipBad.js:7:11:7:31 | fileName | | ||
| | ZipSlipBad.js:7:22:7:31 | entry.path | | ||
| | ZipSlipBad.js:8:37:8:44 | fileName | | ||
| edges | ||
| | ZipSlipBad2.js:5:9:5:46 | fileName | ZipSlipBad2.js:6:22:6:29 | fileName | | ||
| | ZipSlipBad2.js:5:20:5:46 | 'output ... ry.path | ZipSlipBad2.js:5:9:5:46 | fileName | | ||
| | ZipSlipBad2.js:5:37:5:46 | entry.path | ZipSlipBad2.js:5:20:5:46 | 'output ... ry.path | | ||
| | ZipSlipBad.js:7:11:7:31 | fileName | ZipSlipBad.js:8:37:8:44 | fileName | | ||
| | ZipSlipBad.js:7:22:7:31 | entry.path | ZipSlipBad.js:7:11:7:31 | fileName | | ||
| #select | ||
| | ZipSlipBad2.js:6:22:6:29 | fileName | ZipSlipBad2.js:5:37:5:46 | entry.path | ZipSlipBad2.js:6:22:6:29 | fileName | Unsanitized zip archive $@, which may contain '..', is used in a file system operation. | ZipSlipBad2.js:5:37:5:46 | entry.path | item path | | ||
| | ZipSlipBad.js:8:37:8:44 | fileName | ZipSlipBad.js:7:22:7:31 | entry.path | ZipSlipBad.js:8:37:8:44 | fileName | Unsanitized zip archive $@, which may contain '..', is used in a file system operation. | ZipSlipBad.js:7:22:7:31 | entry.path | item path | |
1 change: 1 addition & 0 deletions
1
javascript/ql/test/query-tests/Security/CWE-022/ZipSlip/ZipSlip.qlref
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Security/CWE-022/ZipSlip.ql |
9 changes: 9 additions & 0 deletions
9
javascript/ql/test/query-tests/Security/CWE-022/ZipSlip/ZipSlipBad.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const fs = require('fs'); | ||
| const unzip = require('unzip'); | ||
|
|
||
| fs.createReadStream('archive.zip') | ||
| .pipe(unzip.Parse()) | ||
| .on('entry', entry => { | ||
| const fileName = entry.path; | ||
| entry.pipe(fs.createWriteStream(fileName)); | ||
| }); |
8 changes: 8 additions & 0 deletions
8
javascript/ql/test/query-tests/Security/CWE-022/ZipSlip/ZipSlipBad2.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| var fs = require('fs'); | ||
| var unzip = require('unzip'); | ||
| fs.readFile('path/to/archive.zip', function (err, zipContents) { | ||
| unzip.Parse(zipContents).on('entry', function (entry) { | ||
| var fileName = 'output/path/' + entry.path; | ||
| fs.writeFileSync(fileName, entry.contents); | ||
| }); | ||
| }); |
14 changes: 14 additions & 0 deletions
14
javascript/ql/test/query-tests/Security/CWE-022/ZipSlip/ZipSlipGood.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| const fs = require('fs'); | ||
| const unzip = require('unzip'); | ||
|
|
||
| fs.createReadStream('archive.zip') | ||
| .pipe(unzip.Parse()) | ||
| .on('entry', entry => { | ||
| const fileName = entry.path; | ||
| if (fileName.indexOf('..') == -1) { | ||
| entry.pipe(fs.createWriteStream(fileName)); | ||
| } | ||
| else { | ||
| console.log('skipping bad path', fileName); | ||
| } | ||
| }); |
11 changes: 11 additions & 0 deletions
11
javascript/ql/test/query-tests/Security/CWE-022/ZipSlip/externs.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /** | ||
| * @externs | ||
| */ | ||
| var fs = {}; | ||
|
|
||
| /** | ||
| * @param {string} filename | ||
| * @param {*} data | ||
| * @return {void} | ||
| */ | ||
| fs.writeFileSync = function(filename, data) {}; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.