mirror of
https://github.com/github/codeql.git
synced 2026-02-17 15:33:45 +01:00
256 lines
8.2 KiB
Plaintext
256 lines
8.2 KiB
Plaintext
/**
|
|
* @id js/nodejs-stream-pipe-without-error-handling
|
|
* @name Node.js stream pipe without error handling
|
|
* @description Calling `pipe()` on a stream without error handling may silently drop errors and prevent proper propagation.
|
|
* @kind problem
|
|
* @problem.severity warning
|
|
* @precision high
|
|
* @tags quality
|
|
* maintainability
|
|
* error-handling
|
|
* frameworks/nodejs
|
|
*/
|
|
|
|
import javascript
|
|
|
|
/**
|
|
* A call to the `pipe` method on a Node.js stream.
|
|
*/
|
|
class PipeCall extends DataFlow::MethodCallNode {
|
|
PipeCall() {
|
|
this.getMethodName() = "pipe" and
|
|
this.getNumArgument() = [1, 2] and
|
|
not this.getArgument([0, 1]).asExpr() instanceof Function and
|
|
not this.getArgument(0).asExpr() instanceof ObjectExpr and
|
|
not this.getArgument(0).getALocalSource() = getNonNodeJsStreamType()
|
|
}
|
|
|
|
/** Gets the source stream (receiver of the pipe call). */
|
|
DataFlow::Node getSourceStream() { result = this.getReceiver() }
|
|
|
|
/** Gets the destination stream (argument of the pipe call). */
|
|
DataFlow::Node getDestinationStream() { result = this.getArgument(0) }
|
|
}
|
|
|
|
/**
|
|
* Gets a reference to a value that is known to not be a Node.js stream.
|
|
* This is used to exclude pipe calls on non-stream objects from analysis.
|
|
*/
|
|
DataFlow::Node getNonNodeJsStreamType() {
|
|
result = getNonStreamApi().getAValueReachableFromSource()
|
|
}
|
|
|
|
//highland, arktype execa
|
|
API::Node getNonStreamApi() {
|
|
exists(string moduleName |
|
|
moduleName
|
|
.regexpMatch([
|
|
"rxjs(|/.*)", "@strapi(|/.*)", "highland(|/.*)", "execa(|/.*)", "arktype(|/.*)"
|
|
]) and
|
|
result = API::moduleImport(moduleName)
|
|
)
|
|
or
|
|
result = getNonStreamApi().getAMember()
|
|
or
|
|
result = getNonStreamApi().getAParameter().getAParameter()
|
|
or
|
|
result = getNonStreamApi().getReturn()
|
|
or
|
|
result = getNonStreamApi().getPromised()
|
|
}
|
|
|
|
/**
|
|
* Gets the method names used to register event handlers on Node.js streams.
|
|
* These methods are used to attach handlers for events like `error`.
|
|
*/
|
|
string getEventHandlerMethodName() { result = ["on", "once", "addListener"] }
|
|
|
|
/**
|
|
* Gets the method names that are chainable on Node.js streams.
|
|
*/
|
|
string getChainableStreamMethodName() {
|
|
result =
|
|
[
|
|
"setEncoding", "pause", "resume", "unpipe", "destroy", "cork", "uncork", "setDefaultEncoding",
|
|
"off", "removeListener", getEventHandlerMethodName()
|
|
]
|
|
}
|
|
|
|
/**
|
|
* Gets the method names that are not chainable on Node.js streams.
|
|
*/
|
|
string getNonchainableStreamMethodName() {
|
|
result = ["read", "write", "end", "pipe", "unshift", "push", "isPaused", "wrap", "emit"]
|
|
}
|
|
|
|
/**
|
|
* Gets the property names commonly found on Node.js streams.
|
|
*/
|
|
string getStreamPropertyName() {
|
|
result =
|
|
[
|
|
"readable", "writable", "destroyed", "closed", "readableHighWaterMark", "readableLength",
|
|
"readableObjectMode", "readableEncoding", "readableFlowing", "readableEnded", "flowing",
|
|
"writableHighWaterMark", "writableLength", "writableObjectMode", "writableFinished",
|
|
"writableCorked", "writableEnded", "defaultEncoding", "allowHalfOpen", "objectMode",
|
|
"errored", "pending", "autoDestroy", "encoding", "path", "fd", "bytesRead", "bytesWritten",
|
|
"_readableState", "_writableState"
|
|
]
|
|
}
|
|
|
|
/**
|
|
* Gets all method names commonly found on Node.js streams.
|
|
*/
|
|
string getStreamMethodName() {
|
|
result = [getChainableStreamMethodName(), getNonchainableStreamMethodName()]
|
|
}
|
|
|
|
/**
|
|
* A call to register an event handler on a Node.js stream.
|
|
* This includes methods like `on`, `once`, and `addListener`.
|
|
*/
|
|
class StreamEventRegistration extends DataFlow::MethodCallNode {
|
|
StreamEventRegistration() { this.getMethodName() = getEventHandlerMethodName() }
|
|
}
|
|
|
|
/**
|
|
* Models flow relationships between streams and related operations.
|
|
* Connects destination streams to their corresponding pipe call nodes.
|
|
* Connects streams to their chainable methods.
|
|
*/
|
|
predicate streamFlowStep(DataFlow::Node streamNode, DataFlow::Node relatedNode) {
|
|
exists(PipeCall pipe |
|
|
streamNode = pipe.getDestinationStream() and
|
|
relatedNode = pipe
|
|
)
|
|
or
|
|
exists(DataFlow::MethodCallNode chainable |
|
|
chainable.getMethodName() = getChainableStreamMethodName() and
|
|
streamNode = chainable.getReceiver() and
|
|
relatedNode = chainable
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Tracks the result of a pipe call as it flows through the program.
|
|
*/
|
|
private DataFlow::SourceNode pipeResultTracker(DataFlow::TypeTracker t, PipeCall pipe) {
|
|
t.start() and result = pipe
|
|
or
|
|
exists(DataFlow::TypeTracker t2 | result = pipeResultTracker(t2, pipe).track(t2, t))
|
|
}
|
|
|
|
/**
|
|
* Gets a reference to the result of a pipe call.
|
|
*/
|
|
private DataFlow::SourceNode pipeResultRef(PipeCall pipe) {
|
|
result = pipeResultTracker(DataFlow::TypeTracker::end(), pipe)
|
|
}
|
|
|
|
/**
|
|
* Holds if the pipe call result is used to call a non-stream method.
|
|
* Since pipe() returns the destination stream, this finds cases where
|
|
* the destination stream is used with methods not typical of streams.
|
|
*/
|
|
predicate isPipeFollowedByNonStreamMethod(PipeCall pipeCall) {
|
|
exists(DataFlow::MethodCallNode call |
|
|
call = pipeResultRef(pipeCall).getAMethodCall() and
|
|
not call.getMethodName() = getStreamMethodName()
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Holds if the pipe call result is used to access a property that is not typical of streams.
|
|
*/
|
|
predicate isPipeFollowedByNonStreamProperty(PipeCall pipeCall) {
|
|
exists(DataFlow::PropRef propRef |
|
|
propRef = pipeResultRef(pipeCall).getAPropertyRead() and
|
|
not propRef.getPropertyName() = getStreamPropertyName()
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Holds if the pipe call result is used in a non-stream-like way,
|
|
* either by calling non-stream methods or accessing non-stream properties.
|
|
*/
|
|
predicate isPipeFollowedByNonStreamAccess(PipeCall pipeCall) {
|
|
isPipeFollowedByNonStreamMethod(pipeCall) or
|
|
isPipeFollowedByNonStreamProperty(pipeCall)
|
|
}
|
|
|
|
/**
|
|
* Gets a reference to a stream that may be the source of the given pipe call.
|
|
* Uses type back-tracking to trace stream references in the data flow.
|
|
*/
|
|
private DataFlow::SourceNode streamRef(DataFlow::TypeBackTracker t, PipeCall pipeCall) {
|
|
t.start() and
|
|
result = pipeCall.getSourceStream().getALocalSource()
|
|
or
|
|
exists(DataFlow::SourceNode prev |
|
|
prev = streamRef(t.continue(), pipeCall) and
|
|
streamFlowStep(result.getALocalUse(), prev)
|
|
)
|
|
or
|
|
exists(DataFlow::TypeBackTracker t2 | result = streamRef(t2, pipeCall).backtrack(t2, t))
|
|
}
|
|
|
|
/**
|
|
* Gets a reference to a stream that may be the source of the given pipe call.
|
|
*/
|
|
private DataFlow::SourceNode streamRef(PipeCall pipeCall) {
|
|
result = streamRef(DataFlow::TypeBackTracker::end(), pipeCall)
|
|
}
|
|
|
|
/**
|
|
* Holds if the source stream of the given pipe call has an `error` handler registered.
|
|
*/
|
|
predicate hasErrorHandlerRegistered(PipeCall pipeCall) {
|
|
exists(StreamEventRegistration handler |
|
|
handler = streamRef(pipeCall).getAMethodCall(getEventHandlerMethodName()) and
|
|
handler.getArgument(0).getStringValue() = "error"
|
|
)
|
|
or
|
|
hasPlumber(pipeCall)
|
|
}
|
|
|
|
/**
|
|
* Holds if one of the arguments of the pipe call is a `gulp-plumber` monkey patch.
|
|
*/
|
|
predicate hasPlumber(PipeCall pipeCall) {
|
|
streamRef+(pipeCall) = API::moduleImport("gulp-plumber").getACall()
|
|
}
|
|
|
|
/**
|
|
* Holds if the source or destination of the given pipe call is identified as a non-Node.js stream.
|
|
*/
|
|
predicate hasNonNodeJsStreamSource(PipeCall pipeCall) {
|
|
streamRef(pipeCall) = getNonNodeJsStreamType() or
|
|
pipeResultRef(pipeCall) = getNonNodeJsStreamType()
|
|
}
|
|
|
|
/**
|
|
* Holds if the source stream of the given pipe call is used in a non-stream-like way.
|
|
*/
|
|
private predicate hasNonStreamSourceLikeUsage(PipeCall pipeCall) {
|
|
exists(DataFlow::MethodCallNode call, string name |
|
|
call.getReceiver().getALocalSource() = streamRef(pipeCall) and
|
|
name = call.getMethodName() and
|
|
not name = getStreamMethodName()
|
|
)
|
|
or
|
|
exists(DataFlow::PropRef propRef, string propName |
|
|
propRef.getBase().getALocalSource() = streamRef(pipeCall) and
|
|
propName = propRef.getPropertyName() and
|
|
not propName = [getStreamPropertyName(), getStreamMethodName()]
|
|
)
|
|
}
|
|
|
|
from PipeCall pipeCall
|
|
where
|
|
not hasErrorHandlerRegistered(pipeCall) and
|
|
not isPipeFollowedByNonStreamAccess(pipeCall) and
|
|
not hasNonStreamSourceLikeUsage(pipeCall) and
|
|
not hasNonNodeJsStreamSource(pipeCall)
|
|
select pipeCall,
|
|
"Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped."
|