Files
codeql/javascript/ql/src/Quality/UnhandledStreamPipe.ql
2025-05-26 14:23:20 +02:00

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."