-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathUnhandledErrorInStreamPipeline.ql
More file actions
303 lines (277 loc) · 10.2 KB
/
UnhandledErrorInStreamPipeline.ql
File metadata and controls
303 lines (277 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/**
* @id js/unhandled-error-in-stream-pipeline
* @name Unhandled error in stream pipeline
* @description Calling `pipe()` on a stream without error handling will drop errors coming from the input stream
* @kind problem
* @problem.severity warning
* @precision high
* @tags quality
* reliability
* error-handling
* frameworks/nodejs
*/
import javascript
import semmle.javascript.filters.ClassifyFiles
/**
* 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.
*/
private DataFlow::Node getNonNodeJsStreamType() {
result = getNonStreamApi().getAValueReachableFromSource()
}
/**
* Gets API nodes from modules that are known to not provide Node.js streams.
* This includes reactive programming libraries, frontend frameworks, and other non-stream APIs.
*/
private API::Node getNonStreamApi() {
exists(string moduleName |
moduleName
.regexpMatch([
"rxjs(|/.*)", "@strapi(|/.*)", "highland(|/.*)", "execa(|/.*)", "arktype(|/.*)",
"@ngrx(|/.*)", "@datorama(|/.*)", "@angular(|/.*)", "react.*", "@langchain(|/.*)",
]) 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`.
*/
private string getEventHandlerMethodName() { result = ["on", "once", "addListener"] }
/**
* Gets the method names that are chainable on Node.js streams.
*/
private 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.
*/
private string getNonchainableStreamMethodName() {
result = ["read", "write", "end", "pipe", "unshift", "push", "isPaused", "wrap", "emit"]
}
/**
* Gets the property names commonly found on Node.js streams.
*/
private 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.
*/
private 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 ErrorHandlerRegistration extends DataFlow::MethodCallNode {
ErrorHandlerRegistration() {
this.getMethodName() = getEventHandlerMethodName() and
this.getArgument(0).getStringValue() = "error"
}
}
/**
* Holds if the stream in `node1` will propagate to `node2`.
*/
private predicate streamFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(PipeCall pipe |
node1 = pipe.getDestinationStream() and
node2 = pipe
)
or
exists(DataFlow::MethodCallNode chainable |
chainable.getMethodName() = getChainableStreamMethodName() and
node1 = chainable.getReceiver() and
node2 = chainable
)
}
/**
* Tracks the result of a pipe call as it flows through the program.
*/
private DataFlow::SourceNode destinationStreamRef(DataFlow::TypeTracker t, PipeCall pipe) {
t.start() and
(result = pipe or result = pipe.getDestinationStream().getALocalSource())
or
exists(DataFlow::SourceNode prev |
prev = destinationStreamRef(t.continue(), pipe) and
streamFlowStep(prev, result)
)
or
exists(DataFlow::TypeTracker t2 | result = destinationStreamRef(t2, pipe).track(t2, t))
}
/**
* Gets a reference to the result of a pipe call.
*/
private DataFlow::SourceNode destinationStreamRef(PipeCall pipe) {
result = destinationStreamRef(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.
*/
private predicate isPipeFollowedByNonStreamMethod(PipeCall pipeCall) {
exists(DataFlow::MethodCallNode call |
call = destinationStreamRef(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.
*/
private predicate isPipeFollowedByNonStreamProperty(PipeCall pipeCall) {
exists(DataFlow::PropRef propRef |
propRef = destinationStreamRef(pipeCall).getAPropertyRead() and
not propRef.getPropertyName() = [getStreamPropertyName(), getStreamMethodName()]
)
}
/**
* 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.
*/
private 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 sourceStreamRef(DataFlow::TypeBackTracker t, PipeCall pipeCall) {
t.start() and
result = pipeCall.getSourceStream().getALocalSource()
or
exists(DataFlow::SourceNode prev |
prev = sourceStreamRef(t.continue(), pipeCall) and
streamFlowStep(result.getALocalUse(), prev)
)
or
exists(DataFlow::TypeBackTracker t2 | result = sourceStreamRef(t2, pipeCall).backtrack(t2, t))
}
/**
* Gets a reference to a stream that may be the source of the given pipe call.
*/
private DataFlow::SourceNode sourceStreamRef(PipeCall pipeCall) {
result = sourceStreamRef(DataFlow::TypeBackTracker::end(), pipeCall)
}
/**
* Holds if the source stream of the given pipe call has an `error` handler registered.
*/
private predicate hasErrorHandlerRegistered(PipeCall pipeCall) {
exists(DataFlow::Node stream |
stream = sourceStreamRef(pipeCall).getALocalUse() and
(
stream.(DataFlow::SourceNode).getAMethodCall(_) instanceof ErrorHandlerRegistration
or
exists(DataFlow::SourceNode base, string propName |
stream = base.getAPropertyRead(propName) and
base.getAPropertyRead(propName).getAMethodCall(_) instanceof ErrorHandlerRegistration
)
or
exists(DataFlow::PropWrite propWrite, DataFlow::SourceNode instance |
propWrite.getRhs().getALocalSource() = stream and
instance = propWrite.getBase().getALocalSource() and
instance.getAPropertyRead(propWrite.getPropertyName()).getAMethodCall(_) instanceof
ErrorHandlerRegistration
)
)
)
or
hasPlumber(pipeCall)
}
/**
* Holds if the pipe call uses `gulp-plumber`, which automatically handles stream errors.
* `gulp-plumber` returns a stream that uses monkey-patching to ensure all subsequent streams in the pipeline propagate their errors.
*/
private predicate hasPlumber(PipeCall pipeCall) {
pipeCall.getDestinationStream().getALocalSource() = API::moduleImport("gulp-plumber").getACall()
or
sourceStreamRef+(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.
*/
private predicate hasNonNodeJsStreamSource(PipeCall pipeCall) {
sourceStreamRef(pipeCall) = getNonNodeJsStreamType() or
destinationStreamRef(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() = sourceStreamRef(pipeCall) and
name = call.getMethodName() and
not name = getStreamMethodName()
)
or
exists(DataFlow::PropRef propRef, string propName |
propRef.getBase().getALocalSource() = sourceStreamRef(pipeCall) and
propName = propRef.getPropertyName() and
not propName = [getStreamPropertyName(), getStreamMethodName()]
)
}
/**
* Holds if the pipe call destination stream has an error handler registered.
*/
private predicate hasErrorHandlerDownstream(PipeCall pipeCall) {
exists(DataFlow::SourceNode stream |
stream = destinationStreamRef(pipeCall) and
(
exists(ErrorHandlerRegistration handler | handler.getReceiver().getALocalSource() = stream)
or
exists(DataFlow::SourceNode base, string propName |
stream = base.getAPropertyRead(propName) and
base.getAPropertyRead(propName).getAMethodCall(_) instanceof ErrorHandlerRegistration
)
)
)
}
from PipeCall pipeCall
where
not hasErrorHandlerRegistered(pipeCall) and
hasErrorHandlerDownstream(pipeCall) and
not isPipeFollowedByNonStreamAccess(pipeCall) and
not hasNonStreamSourceLikeUsage(pipeCall) and
not hasNonNodeJsStreamSource(pipeCall) and
not isTestFile(pipeCall.getFile())
select pipeCall,
"Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped."