Skip to content

Commit aff6f00

Browse files
committed
comments improvement,separate module file, fix tests
1 parent 5a49f6b commit aff6f00

File tree

5 files changed

+489
-379
lines changed

5 files changed

+489
-379
lines changed

javascript/ql/src/experimental/Security/CWE-522-DecompressionBombs/DecompressionBombs.ql

Lines changed: 5 additions & 345 deletions
Original file line numberDiff line numberDiff line change
@@ -8,363 +8,23 @@
88
* @id js/user-controlled-data-decompression
99
* @tags security
1010
* experimental
11-
* external/cwe/cwe-409
11+
* external/cwe/cwe-522
1212
*/
1313

1414
import javascript
1515
import semmle.javascript.frameworks.ReadableStream
1616
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
35918

36019
class BombConfiguration extends TaintTracking::Configuration {
36120
BombConfiguration() { this = "DecompressionBombs" }
36221

363-
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
22+
override predicate isSource(DataFlow::Node source) {
23+
source instanceof RemoteFlowSource
24+
}
36425

36526
override predicate isSink(DataFlow::Node sink) {
36627
sink instanceof DecompressionBomb::Sink
367-
// any()
36828
}
36929

37030
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {

0 commit comments

Comments
 (0)