Skip to content

Commit 2d0067d

Browse files
committed
fix some qldocs, change Sink extenstion model, deduct some not necessarily checks :)
1 parent 4283bb7 commit 2d0067d

File tree

4 files changed

+161
-201
lines changed

4 files changed

+161
-201
lines changed

python/ql/src/experimental/Security/CWE-409/DecompressionBombs.ql

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import semmle.python.ApiGraphs
1818
import semmle.python.dataflow.new.RemoteFlowSources
1919
import semmle.python.dataflow.new.internal.DataFlowPublic
2020
import experimental.semmle.python.security.DecompressionBomb
21+
import FileAndFormRemoteFlowSource::FileAndFormRemoteFlowSource
2122

2223
/**
2324
* `io.TextIOWrapper(ip, encoding='utf-8')` like following:
@@ -39,59 +40,12 @@ predicate isAdditionalTaintStepTextIOWrapper(DataFlow::Node nodeFrom, DataFlow::
3940
)
4041
}
4142

42-
module FileAndFormRemoteFlowSource {
43-
class FastAPI extends RemoteFlowSource::Range {
44-
FastAPI() {
45-
exists(API::Node fastApiParam, Expr fastApiUploadFile |
46-
fastApiParam =
47-
API::moduleImport("fastapi")
48-
.getMember("FastAPI")
49-
.getReturn()
50-
.getMember("post")
51-
.getReturn()
52-
.getParameter(0)
53-
.getKeywordParameter(_) and
54-
fastApiUploadFile =
55-
API::moduleImport("fastapi")
56-
.getMember("UploadFile")
57-
.getASubclass*()
58-
.getAValueReachableFromSource()
59-
.asExpr()
60-
|
61-
fastApiUploadFile =
62-
fastApiParam.asSource().asExpr().(Parameter).getAnnotation().getASubExpression*() and
63-
// Multiple Uploaded files as list of fastapi.UploadFile
64-
exists(For f, Attribute attr |
65-
fastApiParam.getAValueReachableFromSource().asExpr() = f.getIter().getASubExpression*()
66-
|
67-
TaintTracking::localExprTaint(f.getIter(), attr.getObject()) and
68-
attr.getName() = ["filename", "content_type", "headers", "file", "read"] and
69-
this.asExpr() = attr
70-
)
71-
or
72-
// one Uploaded file as fastapi.UploadFile
73-
this =
74-
[
75-
fastApiParam.getMember(["filename", "content_type", "headers"]).asSource(),
76-
fastApiParam
77-
.getMember("file")
78-
.getMember(["readlines", "readline", "read"])
79-
.getReturn()
80-
.asSource(), fastApiParam.getMember("read").getReturn().asSource()
81-
]
82-
)
83-
}
84-
85-
override string getSourceType() { result = "fastapi HTTP FORM files" }
86-
}
87-
}
88-
8943
module BombsConfig implements DataFlow::ConfigSig {
9044
predicate isSource(DataFlow::Node source) {
9145
(
9246
source instanceof RemoteFlowSource
9347
or
94-
source instanceof FileAndFormRemoteFlowSource::FastAPI
48+
source instanceof FastAPI
9549
) and
9650
not source.getLocation().getFile().inStdlib() and
9751
not source.getLocation().getFile().getRelativePath().matches("%venv%")
@@ -113,11 +67,11 @@ module BombsConfig implements DataFlow::ConfigSig {
11367
}
11468
}
11569

116-
module Bombs = TaintTracking::Global<BombsConfig>;
70+
module BombsFlow = TaintTracking::Global<BombsConfig>;
11771

118-
import Bombs::PathGraph
72+
import BombsFlow::PathGraph
11973

120-
from Bombs::PathNode source, Bombs::PathNode sink
121-
where Bombs::flowPath(source, sink)
74+
from BombsFlow::PathNode source, BombsFlow::PathNode sink
75+
where BombsFlow::flowPath(source, sink)
12276
select sink.getNode(), source, sink, "This uncontrolled file extraction is $@.", source.getNode(),
12377
"depends on this user controlled data"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import python
2+
import semmle.python.dataflow.new.DataFlow
3+
import semmle.python.dataflow.new.TaintTracking
4+
import semmle.python.ApiGraphs
5+
6+
/**
7+
* Provides user-controllable Remote sources for file(s) upload and Multipart-Form
8+
*/
9+
module FileAndFormRemoteFlowSource {
10+
/**
11+
* A
12+
*/
13+
class FastAPI extends DataFlow::Node {
14+
FastAPI() {
15+
exists(API::Node fastApiParam, Expr fastApiUploadFile |
16+
fastApiParam =
17+
API::moduleImport("fastapi")
18+
.getMember("FastAPI")
19+
.getReturn()
20+
.getMember("post")
21+
.getReturn()
22+
.getParameter(0)
23+
.getKeywordParameter(_) and
24+
fastApiUploadFile =
25+
API::moduleImport("fastapi")
26+
.getMember("UploadFile")
27+
.getASubclass*()
28+
.getAValueReachableFromSource()
29+
.asExpr()
30+
|
31+
// Multiple uploaded files as list of fastapi.UploadFile
32+
// @app.post("/")
33+
// def upload(files: List[UploadFile] = File(...)):
34+
// for file in files:
35+
fastApiUploadFile =
36+
fastApiParam.asSource().asExpr().(Parameter).getAnnotation().getASubExpression*() and
37+
exists(For f, Attribute attr |
38+
fastApiParam.getAValueReachableFromSource().asExpr() = f.getIter().getASubExpression*()
39+
|
40+
TaintTracking::localExprTaint(f.getIter(), attr.getObject()) and
41+
attr.getName() = ["filename", "content_type", "headers", "file", "read"] and
42+
this.asExpr() = attr
43+
)
44+
or
45+
// One uploaded file as fastapi.UploadFile
46+
// @app.post("/zipbomb2")
47+
// async def zipbomb2(file: UploadFile):
48+
// print(file.filename)
49+
this =
50+
[
51+
fastApiParam.getMember(["filename", "content_type", "headers"]).asSource(),
52+
fastApiParam
53+
.getMember("file")
54+
.getMember(["readlines", "readline", "read"])
55+
.getReturn()
56+
.asSource(), fastApiParam.getMember("read").getReturn().asSource()
57+
]
58+
)
59+
}
60+
61+
string getSourceType() { result = "fastapi HTTP FORM files" }
62+
}
63+
}

python/ql/src/experimental/Security/CWE-409/example_good.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,33 @@
22

33

44
def safeUnzip(zipFileName):
5-
buffer_size = 2 ** 10 * 8
5+
'''
6+
safeUnzip reads each file inside the zipfile 1 MB by 1 MB
7+
and during extraction or reading of these files it checks the total decompressed size
8+
doesn't exceed the SIZE_THRESHOLD
9+
'''
10+
buffer_size = 1024 * 1024 * 1 # 1 MB
611
total_size = 0
7-
SIZE_THRESHOLD = 2 ** 10 * 100
12+
SIZE_THRESHOLD = 1024 * 1024 * 10 # 10 MB
813
with zipfile.ZipFile(zipFileName) as myzip:
914
for fileinfo in myzip.infolist():
10-
content = b''
1115
with myzip.open(fileinfo.filename, mode="r") as myfile:
16+
content = b''
1217
chunk = myfile.read(buffer_size)
18+
total_size += buffer_size
19+
if total_size > SIZE_THRESHOLD:
20+
print("Bomb detected")
21+
return False # it isn't a successful extract or read
22+
content += chunk
23+
# reading next bytes of uncompressed data
1324
while chunk:
1425
chunk = myfile.read(buffer_size)
1526
total_size += buffer_size
1627
if total_size > SIZE_THRESHOLD:
1728
print("Bomb detected")
18-
return
29+
return False # it isn't a successful extract or read
1930
content += chunk
31+
32+
# An example of extracting or reading each decompressed file here
2033
print(bytes.decode(content, 'utf-8'))
21-
return "No Bombs!"
34+
return True # it is a successful extract or read

0 commit comments

Comments
 (0)