|
8 | 8 | * @id js/user-controlled-data-decompression
|
9 | 9 | * @tags security
|
10 | 10 | * experimental
|
11 |
| - * external/cwe/cwe-409 |
| 11 | + * external/cwe/cwe-522 |
12 | 12 | */
|
13 | 13 |
|
14 | 14 | import javascript
|
15 | 15 | import semmle.javascript.frameworks.ReadableStream
|
16 | 16 | import DataFlow::PathGraph
|
17 |
| - |
18 |
| -module DecompressionBomb { |
19 |
| - /** |
20 |
| - * the Sinks of uncontrolled data decompression |
21 |
| - */ |
22 |
| - class Sink extends DataFlow::Node { |
23 |
| - Sink() { this = any(Range r).sink() } |
24 |
| - } |
25 |
| - |
26 |
| - /** |
27 |
| - * The additional taint steps that need for creating taint tracking or dataflow. |
28 |
| - */ |
29 |
| - abstract class AdditionalTaintStep extends string { |
30 |
| - AdditionalTaintStep() { this = "AdditionalTaintStep" } |
31 |
| - |
32 |
| - /** |
33 |
| - * Holds if there is a additional taint step between pred and succ. |
34 |
| - */ |
35 |
| - abstract predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ); |
36 |
| - } |
37 |
| - |
38 |
| - /** |
39 |
| - * A abstract class responsible for extending new decompression sinks |
40 |
| - */ |
41 |
| - abstract private class Range extends API::Node { |
42 |
| - /** |
43 |
| - * Gets the sink of responsible for decompression node |
44 |
| - * |
45 |
| - * it can be a path, stream of compressed data, |
46 |
| - * or a call to function that use pipe |
47 |
| - */ |
48 |
| - abstract DataFlow::Node sink(); |
49 |
| - } |
50 |
| - |
51 |
| - module ReadableStream { |
52 |
| - class ReadableStreamAdditionalTaintStep extends AdditionalTaintStep { |
53 |
| - ReadableStreamAdditionalTaintStep() { this = "AdditionalTaintStep" } |
54 |
| - |
55 |
| - override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { |
56 |
| - // additional taint step for fs.readFile(pred) |
57 |
| - // It can be global additional step too |
58 |
| - exists(DataFlow::CallNode n | n = DataFlow::moduleMember("fs", "readFile").getACall() | |
59 |
| - pred = n.getArgument(0) and succ = n.getABoundCallbackParameter(1, 1) |
60 |
| - ) |
61 |
| - or |
62 |
| - readablePipeAdditionalTaintStep(pred, succ) |
63 |
| - or |
64 |
| - streamPipelineAdditionalTaintStep(pred, succ) |
65 |
| - or |
66 |
| - promisesFileHandlePipeAdditionalTaintStep(pred, succ) |
67 |
| - or |
68 |
| - exists(FileSystemReadAccess cn | |
69 |
| - pred = cn.getAPathArgument() and |
70 |
| - succ = cn.getADataNode() |
71 |
| - ) |
72 |
| - } |
73 |
| - } |
74 |
| - } |
75 |
| - |
76 |
| - module JsZip { |
77 |
| - /** |
78 |
| - * The decompression sinks of [jszip](https://www.npmjs.com/package/jszip) package |
79 |
| - */ |
80 |
| - class DecompressionBomb extends Range { |
81 |
| - DecompressionBomb() { this = API::moduleImport("jszip").getMember("loadAsync") } |
82 |
| - |
83 |
| - override DataFlow::Node sink() { |
84 |
| - result = this.getParameter(0).asSink() and not this.sanitizer(this) |
85 |
| - } |
86 |
| - |
87 |
| - /** |
88 |
| - * Gets a jszip `loadAsync` instance |
89 |
| - * and Holds if member of name `uncompressedSize` exists |
90 |
| - */ |
91 |
| - predicate sanitizer(API::Node loadAsync) { |
92 |
| - exists(loadAsync.getASuccessor*().getMember("_data").getMember("uncompressedSize")) |
93 |
| - } |
94 |
| - } |
95 |
| - } |
96 |
| - |
97 |
| - module NodeTar { |
98 |
| - /** |
99 |
| - * The decompression sinks of [node-tar](https://www.npmjs.com/package/tar) package |
100 |
| - */ |
101 |
| - class DecompressionBomb extends Range { |
102 |
| - DecompressionBomb() { this = API::moduleImport("tar").getMember(["x", "extract"]) } |
103 |
| - |
104 |
| - override DataFlow::Node sink() { |
105 |
| - ( |
106 |
| - // piping tar.x() |
107 |
| - result = this.getACall() |
108 |
| - or |
109 |
| - // tar.x({file: filename}) |
110 |
| - result = this.getParameter(0).getMember("file").asSink() |
111 |
| - ) and |
112 |
| - // and there shouldn't be a "maxReadSize: ANum" option |
113 |
| - not this.sanitizer(this.getParameter(0)) |
114 |
| - } |
115 |
| - |
116 |
| - /** |
117 |
| - * Gets a options parameter that belong to a `tar` instance |
118 |
| - * and Holds if "maxReadSize: ANumber" option exists |
119 |
| - */ |
120 |
| - predicate sanitizer(API::Node tarExtract) { exists(tarExtract.getMember("maxReadSize")) } |
121 |
| - } |
122 |
| - |
123 |
| - class DecompressionAdditionalSteps extends AdditionalTaintStep { |
124 |
| - DecompressionAdditionalSteps() { this = "AdditionalTaintStep" } |
125 |
| - |
126 |
| - override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { |
127 |
| - exists(API::Node n | n = API::moduleImport("tar") | |
128 |
| - pred = n.asSource() and |
129 |
| - ( |
130 |
| - succ = n.getMember("x").getACall() or |
131 |
| - succ = n.getMember("x").getACall().getArgument(0) |
132 |
| - ) |
133 |
| - ) |
134 |
| - } |
135 |
| - } |
136 |
| - } |
137 |
| - |
138 |
| - module Zlib { |
139 |
| - /** |
140 |
| - * The decompression sinks of `node:zlib` |
141 |
| - */ |
142 |
| - class DecompressionBomb extends Range { |
143 |
| - boolean isSynk; |
144 |
| - |
145 |
| - DecompressionBomb() { |
146 |
| - this = |
147 |
| - API::moduleImport("zlib") |
148 |
| - .getMember([ |
149 |
| - "gunzip", "gunzipSync", "unzip", "unzipSync", "brotliDecompress", |
150 |
| - "brotliDecompressSync", "inflateSync", "inflateRawSync", "inflate", "inflateRaw" |
151 |
| - ]) and |
152 |
| - isSynk = true |
153 |
| - or |
154 |
| - this = |
155 |
| - API::moduleImport("zlib") |
156 |
| - .getMember([ |
157 |
| - "createGunzip", "createBrotliDecompress", "createUnzip", "createInflate", |
158 |
| - "createInflateRaw" |
159 |
| - ]) and |
160 |
| - isSynk = false |
161 |
| - } |
162 |
| - |
163 |
| - override DataFlow::Node sink() { |
164 |
| - result = this.getACall() and |
165 |
| - not this.sanitizer(this.getParameter(0)) and |
166 |
| - isSynk = false |
167 |
| - or |
168 |
| - result = this.getACall().getArgument(0) and |
169 |
| - not this.sanitizer(this.getParameter(1)) and |
170 |
| - isSynk = true |
171 |
| - } |
172 |
| - |
173 |
| - /** |
174 |
| - * Gets a options parameter that belong to a zlib instance |
175 |
| - * and Holds if "maxOutputLength: ANumber" option exists |
176 |
| - */ |
177 |
| - predicate sanitizer(API::Node zlib) { exists(zlib.getMember("maxOutputLength")) } |
178 |
| - } |
179 |
| - } |
180 |
| - |
181 |
| - module Pako { |
182 |
| - /** |
183 |
| - * The decompression sinks of (pako)[https://www.npmjs.com/package/pako] |
184 |
| - */ |
185 |
| - class DecompressionBomb extends Range { |
186 |
| - DecompressionBomb() { |
187 |
| - this = API::moduleImport("pako").getMember(["inflate", "inflateRaw", "ungzip"]) |
188 |
| - } |
189 |
| - |
190 |
| - override DataFlow::Node sink() { result = this.getParameter(0).asSink() } |
191 |
| - } |
192 |
| - |
193 |
| - class DecompressionAdditionalSteps extends AdditionalTaintStep { |
194 |
| - DecompressionAdditionalSteps() { this = "AdditionalTaintStep" } |
195 |
| - |
196 |
| - override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { |
197 |
| - // succ = new Uint8Array(pred) |
198 |
| - exists(DataFlow::Node n, NewExpr ne | ne = n.asExpr() | |
199 |
| - pred.asExpr() = ne.getArgument(0) and |
200 |
| - succ.asExpr() = ne and |
201 |
| - ne.getCalleeName() = "Uint8Array" |
202 |
| - ) |
203 |
| - } |
204 |
| - } |
205 |
| - } |
206 |
| - |
207 |
| - module AdmZip { |
208 |
| - /** |
209 |
| - * The decompression sinks of (adm-zip)[https://www.npmjs.com/package/adm-zip] |
210 |
| - */ |
211 |
| - class DecompressionBomb extends Range { |
212 |
| - DecompressionBomb() { this = API::moduleImport("adm-zip").getInstance() } |
213 |
| - |
214 |
| - override DataFlow::Node sink() { |
215 |
| - result = |
216 |
| - this.getMember(["extractAllTo", "extractEntryTo", "readAsText"]).getReturn().asSource() |
217 |
| - or |
218 |
| - result = this.getASuccessor*().getMember("getData").getReturn().asSource() |
219 |
| - } |
220 |
| - } |
221 |
| - |
222 |
| - class DecompressionAdditionalSteps extends AdditionalTaintStep { |
223 |
| - DecompressionAdditionalSteps() { this = "AdditionalTaintStep" } |
224 |
| - |
225 |
| - override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { |
226 |
| - exists(API::Node n | n = API::moduleImport("adm-zip") | |
227 |
| - pred = n.getParameter(0).asSink() and |
228 |
| - ( |
229 |
| - succ = |
230 |
| - n.getInstance() |
231 |
| - .getMember(["extractAllTo", "extractEntryTo", "readAsText"]) |
232 |
| - .getReturn() |
233 |
| - .asSource() |
234 |
| - or |
235 |
| - succ = |
236 |
| - n.getInstance() |
237 |
| - .getMember("getEntries") |
238 |
| - .getASuccessor*() |
239 |
| - .getMember("getData") |
240 |
| - .getReturn() |
241 |
| - .asSource() |
242 |
| - ) |
243 |
| - ) |
244 |
| - } |
245 |
| - } |
246 |
| - } |
247 |
| - |
248 |
| - module Decompress { |
249 |
| - /** |
250 |
| - * The decompression sinks of (decompress)[https://www.npmjs.com/package/decompress] |
251 |
| - */ |
252 |
| - class DecompressionBomb extends Range { |
253 |
| - DecompressionBomb() { this = API::moduleImport("decompress") } |
254 |
| - |
255 |
| - override DataFlow::Node sink() { result = this.getACall().getArgument(0) } |
256 |
| - } |
257 |
| - } |
258 |
| - |
259 |
| - module GunzipMaybe { |
260 |
| - /** |
261 |
| - * The decompression sinks of (gunzip-maybe)[https://www.npmjs.com/package/gunzip-maybe] |
262 |
| - */ |
263 |
| - class DecompressionBomb extends Range { |
264 |
| - DecompressionBomb() { this = API::moduleImport("gunzip-maybe") } |
265 |
| - |
266 |
| - override DataFlow::Node sink() { result = this.getACall() } |
267 |
| - } |
268 |
| - } |
269 |
| - |
270 |
| - module Unbzip2Stream { |
271 |
| - /** |
272 |
| - * The decompression sinks of (unbzip2-stream)[https://www.npmjs.com/package/unbzip2-stream] |
273 |
| - */ |
274 |
| - class DecompressionBomb extends Range { |
275 |
| - DecompressionBomb() { this = API::moduleImport("unbzip2-stream") } |
276 |
| - |
277 |
| - override DataFlow::Node sink() { result = this.getACall() } |
278 |
| - } |
279 |
| - } |
280 |
| - |
281 |
| - module Unzipper { |
282 |
| - /** |
283 |
| - * The decompression sinks of (unzipper)[https://www.npmjs.com/package/unzipper] |
284 |
| - */ |
285 |
| - class DecompressionBomb extends Range { |
286 |
| - string funcName; |
287 |
| - |
288 |
| - DecompressionBomb() { |
289 |
| - this = API::moduleImport("unzipper").getMember(["Extract", "Parse", "ParseOne"]) and |
290 |
| - funcName = ["Extract", "Parse", "ParseOne"] |
291 |
| - or |
292 |
| - this = API::moduleImport("unzipper").getMember("Open") and |
293 |
| - // open has some functions which will be specified in sink predicate |
294 |
| - funcName = "Open" |
295 |
| - } |
296 |
| - |
297 |
| - override DataFlow::Node sink() { |
298 |
| - result = this.getMember(["buffer", "file", "url", "file"]).getACall().getArgument(0) and |
299 |
| - funcName = "Open" |
300 |
| - or |
301 |
| - result = this.getACall() and |
302 |
| - funcName = ["Extract", "Parse", "ParseOne"] |
303 |
| - } |
304 |
| - |
305 |
| - /** |
306 |
| - * Gets a |
307 |
| - * and Holds if unzipper instance has a member `uncompressedSize` |
308 |
| - * |
309 |
| - * it is really difficult to implement this sanitizer, |
310 |
| - * so i'm going to check if there is a member like `vars.uncompressedSize` in whole DB or not! |
311 |
| - */ |
312 |
| - predicate sanitizer() { |
313 |
| - exists(this.getASuccessor*().getMember("vars").getMember("uncompressedSize")) and |
314 |
| - funcName = ["Extract", "Parse", "ParseOne"] |
315 |
| - } |
316 |
| - } |
317 |
| - } |
318 |
| - |
319 |
| - module Yauzl { |
320 |
| - /** |
321 |
| - * The decompression sinks of (yauzl)[https://www.npmjs.com/package/yauzl] |
322 |
| - */ |
323 |
| - class DecompressionBomb extends Range { |
324 |
| - // open function has a sanitizer which we should label it with this boolean |
325 |
| - boolean isOpenFunc; |
326 |
| - |
327 |
| - DecompressionBomb() { |
328 |
| - this = |
329 |
| - API::moduleImport("yauzl") |
330 |
| - .getMember([ |
331 |
| - "fromFd", "fromBuffer", "fromRandomAccessReader", "fromRandomAccessReader" |
332 |
| - ]) and |
333 |
| - isOpenFunc = false |
334 |
| - or |
335 |
| - this = API::moduleImport("yauzl").getMember("open") and |
336 |
| - isOpenFunc = true |
337 |
| - } |
338 |
| - |
339 |
| - override DataFlow::Node sink() { |
340 |
| - result = this.getASuccessor*().getMember("readEntry").getACall() and |
341 |
| - not this.sanitizer() and |
342 |
| - isOpenFunc = true |
343 |
| - or |
344 |
| - result = this.getACall().getArgument(0) and |
345 |
| - isOpenFunc = false |
346 |
| - } |
347 |
| - |
348 |
| - /** |
349 |
| - * Gets a |
350 |
| - * and Holds if yauzl `open` instance has a member `uncompressedSize` |
351 |
| - */ |
352 |
| - predicate sanitizer() { |
353 |
| - exists(this.getASuccessor*().getMember("uncompressedSize")) and |
354 |
| - isOpenFunc = true |
355 |
| - } |
356 |
| - } |
357 |
| - } |
358 |
| -} |
| 17 | +import DecompressionBombs |
359 | 18 |
|
360 | 19 | class BombConfiguration extends TaintTracking::Configuration {
|
361 | 20 | BombConfiguration() { this = "DecompressionBombs" }
|
362 | 21 |
|
363 |
| - override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } |
| 22 | + override predicate isSource(DataFlow::Node source) { |
| 23 | + source instanceof RemoteFlowSource |
| 24 | + } |
364 | 25 |
|
365 | 26 | override predicate isSink(DataFlow::Node sink) {
|
366 | 27 | sink instanceof DecompressionBomb::Sink
|
367 |
| - // any() |
368 | 28 | }
|
369 | 29 |
|
370 | 30 | override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
|
0 commit comments