-
Notifications
You must be signed in to change notification settings - Fork 1.7k
JS: add new query: js/unclosed-stream #3682
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
Draft
esbena
wants to merge
2
commits into
github:main
Choose a base branch
from
esbena:js/hanging-stream
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
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 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 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,69 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
|
||
<overview> | ||
|
||
<p> | ||
|
||
Callback-based input streams generate calls until they are empty or | ||
closed explicitly, and they will take up system resources until | ||
that point in time. | ||
|
||
</p> | ||
|
||
<p> | ||
|
||
Forgetting to close a stream that no longer is useful can cause | ||
unnecessary exhaustion of system resources, which may cause the | ||
application to be unresponsive or even crash. | ||
|
||
</p> | ||
|
||
</overview> | ||
|
||
<recommendation> | ||
|
||
<p> | ||
|
||
Close input streams explicitly when there is no interest in their | ||
remaining content. | ||
|
||
</p> | ||
|
||
</recommendation> | ||
|
||
<example> | ||
|
||
<p> | ||
|
||
In the following example, an array stores the content of a stream | ||
until the array becomes too big. When that happens, an error message | ||
is generated, and the stream processing is supposed to end. | ||
|
||
</p> | ||
|
||
<sample src="examples/UnclosedStream.js"/> | ||
|
||
<p> | ||
|
||
The stream will however keep invoking the callback until it becomes | ||
empty, regardless of the programmers intention to not process | ||
additional content. This means that the array will grow indefinitely, if a malicious actor keep sending data. | ||
|
||
Close the stream to remedy this: | ||
|
||
</p> | ||
|
||
<sample src="examples/UnclosedStream_fixed.js"/> | ||
|
||
</example> | ||
|
||
<references> | ||
|
||
<li>Node.js: <a href="https://nodejs.org/api/stream.html">Stream</a></li> | ||
|
||
</references> | ||
|
||
</qhelp> |
This file contains 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,128 @@ | ||||||
/** | ||||||
* @name Unclosed stream | ||||||
* @description A stream that is not closed may leak system resources. | ||||||
* @kind problem | ||||||
* @problem.severity error | ||||||
* @precision high | ||||||
* @id js/unclosed-stream | ||||||
* @tags security | ||||||
* external/cwe/cwe-730 | ||||||
* external/cwe/cwe-404 | ||||||
*/ | ||||||
|
||||||
import javascript | ||||||
|
||||||
/** | ||||||
* Gets a function that `caller` invokes directly or indirectly as a callback to a library function. | ||||||
*/ | ||||||
Function getACallee(Function caller) { | ||||||
exists(DataFlow::InvokeNode invk | invk.getEnclosingFunction() = caller | | ||||||
result = invk.getACallee() | ||||||
or | ||||||
not exists(invk.getACallee()) and | ||||||
result = invk.getCallback(invk.getNumArgument()).getFunction() | ||||||
) | ||||||
} | ||||||
|
||||||
/** | ||||||
* A function that can be terminated meaningfully in an asynchronous context. | ||||||
*/ | ||||||
abstract class AsyncTerminatableFunction extends DataFlow::FunctionNode { | ||||||
/** | ||||||
* Gets a node where this function terminates. | ||||||
* | ||||||
* It can be expected that no further expressions in this function will be evaluated after the evaluation of this node. | ||||||
*/ | ||||||
abstract DataFlow::Node getTermination(); | ||||||
|
||||||
/** | ||||||
* Gets a brief description of this function. | ||||||
*/ | ||||||
abstract string getKind(); | ||||||
} | ||||||
|
||||||
/** | ||||||
* A promise executor as a function that can be terminated in an asynchronous context. | ||||||
*/ | ||||||
class PromiseExecutor extends AsyncTerminatableFunction { | ||||||
PromiseDefinition def; | ||||||
|
||||||
PromiseExecutor() { this = def.getExecutor() } | ||||||
|
||||||
override DataFlow::CallNode getTermination() { | ||||||
result = [def.getRejectParameter(), def.getResolveParameter()].getACall() | ||||||
} | ||||||
|
||||||
override string getKind() { result = "promise" } | ||||||
} | ||||||
|
||||||
/** | ||||||
* A callback-invoking function heuristic as a function that can be terminated in an asynchronous context. | ||||||
*/ | ||||||
class FunctionWithCallback extends AsyncTerminatableFunction { | ||||||
DataFlow::ParameterNode callbackParameter; | ||||||
|
||||||
FunctionWithCallback() { | ||||||
// the last parameter is the callback | ||||||
callbackParameter = this.getLastParameter() and | ||||||
// simple escape analysis | ||||||
not exists(DataFlow::Node escape | callbackParameter.flowsTo(escape) | | ||||||
escape = any(DataFlow::PropWrite w).getRhs() or | ||||||
escape = any(DataFlow::CallNode c).getAnArgument() | ||||||
) and | ||||||
// no return value | ||||||
(this.getFunction() instanceof ArrowFunctionExpr or not exists(this.getAReturn())) and | ||||||
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. We could exclude arrow functions with an explicit return, like
Suggested change
|
||||||
// all callback invocations are terminal (note that this permits calls in closures) | ||||||
forex(DataFlow::CallNode termination | termination = callbackParameter.getACall() | | ||||||
termination.asExpr() = any(Function f).getExit().getAPredecessor() | ||||||
) and | ||||||
// avoid confusion with promises | ||||||
not this instanceof PromiseExecutor and | ||||||
not exists(PromiseCandidate c | this.flowsTo(c.getAnArgument())) | ||||||
} | ||||||
|
||||||
override DataFlow::CallNode getTermination() { result = callbackParameter.getACall() } | ||||||
|
||||||
override string getKind() { result = "asynchronous function" } | ||||||
} | ||||||
|
||||||
/** | ||||||
* A data stream. | ||||||
*/ | ||||||
class Stream extends DataFlow::SourceNode { | ||||||
DataFlow::FunctionNode processor; | ||||||
|
||||||
Stream() { | ||||||
exists(DataFlow::CallNode onData | | ||||||
this instanceof EventEmitter and | ||||||
onData = this.getAMethodCall(EventEmitter::on()) and | ||||||
onData.getArgument(0).mayHaveStringValue("data") and | ||||||
processor = onData.getCallback(1) | ||||||
) | ||||||
} | ||||||
|
||||||
/** | ||||||
* Gets a call that closes thes stream. | ||||||
*/ | ||||||
DataFlow::Node getClose() { result = this.getAMethodCall("destroy") } | ||||||
|
||||||
/** | ||||||
* Gets a function that processes the content of the stream. | ||||||
*/ | ||||||
DataFlow::FunctionNode getProcessor() { result = processor } | ||||||
} | ||||||
|
||||||
from | ||||||
AsyncTerminatableFunction terminatable, DataFlow::CallNode termination, Stream stream, | ||||||
Function processor | ||||||
where | ||||||
stream.getAstNode().getContainer() = getACallee*(terminatable.getFunction()) and | ||||||
termination = terminatable.getTermination() and | ||||||
processor = stream.getProcessor().getFunction() and | ||||||
termination.asExpr().getEnclosingFunction() = getACallee*(processor) and | ||||||
not exists(Function destroyFunction | | ||||||
destroyFunction = getACallee*(processor) and | ||||||
stream.getClose().asExpr().getEnclosingFunction() = destroyFunction | ||||||
) | ||||||
select processor, "This stream processor $@ the enclosing $@ without closing the stream.", | ||||||
termination, "terminates", terminatable, terminatable.getKind() |
16 changes: 16 additions & 0 deletions
16
javascript/ql/src/Security/CWE-730/examples/UnclosedStream.js
This file contains 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,16 @@ | ||
var https = require("https"); | ||
|
||
new Promise(function dispatchHttpRequest(resolve, reject) { | ||
https.request(options, function(stream) { | ||
var responseBuffer = []; | ||
stream.on("data", function(chunk) { | ||
responseBuffer.push(chunk); | ||
|
||
if (responseBuffer.length > 1024) { | ||
reject("Input size of 1024 exceeded."); // BAD | ||
} | ||
}); | ||
|
||
stream.on("end", () => resolve(responseBuffer)); | ||
}); | ||
}); |
17 changes: 17 additions & 0 deletions
17
javascript/ql/src/Security/CWE-730/examples/UnclosedStream_fixed.js
This file contains 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 @@ | ||
var https = require("https"); | ||
|
||
new Promise(function dispatchHttpRequest(resolve, reject) { | ||
https.request(options, function(stream) { | ||
var responseBuffer = []; | ||
stream.on("data", function(chunk) { | ||
responseBuffer.push(chunk); | ||
|
||
if (responseBuffer.length > 1024) { | ||
stream.destroy(); | ||
reject("Input size of 1024 exceeded."); // GOOD | ||
} | ||
}); | ||
|
||
stream.on("end", () => resolve(responseBuffer)); | ||
}); | ||
}); |
This file contains 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 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 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 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
2 changes: 2 additions & 0 deletions
2
javascript/ql/test/library-tests/frameworks/NodeJSLib/http.js
This file contains 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,2 @@ | ||
var http = require("http"); | ||
http.request(x, data => data.on("data", d => undefined)); |
This file contains 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
5 changes: 5 additions & 0 deletions
5
javascript/ql/test/query-tests/Security/CWE-730/UnclosedStream.expected
This file contains 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,5 @@ | ||
| unclosed-stream.js:9:23:9:50 | d => re ... silly") | This stream processor $@ the enclosing $@ without closing the stream. | unclosed-stream.js:9:28:9:50 | resolve ... silly") | terminates | unclosed-stream.js:3:13:46:1 | functio ... });\\n} | promise | | ||
| unclosed-stream.js:11:23:11:49 | d => re ... silly") | This stream processor $@ the enclosing $@ without closing the stream. | unclosed-stream.js:11:28:11:49 | rejectP ... silly") | terminates | unclosed-stream.js:3:13:46:1 | functio ... });\\n} | promise | | ||
| unclosed-stream.js:13:23:13:50 | d => re ... silly") | This stream processor $@ the enclosing $@ without closing the stream. | unclosed-stream.js:5:5:5:24 | rejectPromise(value) | terminates | unclosed-stream.js:3:13:46:1 | functio ... });\\n} | promise | | ||
| unclosed-stream.js:35:23:35:50 | d => re ... silly") | This stream processor $@ the enclosing $@ without closing the stream. | unclosed-stream.js:35:28:35:50 | resolve ... silly") | terminates | unclosed-stream.js:3:13:46:1 | functio ... });\\n} | promise | | ||
| unclosed-stream.js:43:23:43:50 | d => re ... silly") | This stream processor $@ the enclosing $@ without closing the stream. | unclosed-stream.js:43:28:43:50 | resolve ... silly") | terminates | unclosed-stream.js:3:13:46:1 | functio ... });\\n} | promise | |
1 change: 1 addition & 0 deletions
1
javascript/ql/test/query-tests/Security/CWE-730/UnclosedStream.qlref
This file contains 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-730/UnclosedStream.ql |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we rephrase this in terms of events, we can avoid inventing non-standard terms: