Skip to content

Commit 4a37da3

Browse files
committed
V1
1 parent 4bf03e7 commit 4a37da3

9 files changed

Lines changed: 522 additions & 0 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @name User-controlled file decompression
3+
* @description User-controlled data that flows into decompression library APIs without checking the compression rate is dangerous
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 7.8
7+
* @precision medium
8+
* @id cpp/user-controlled-file-decompression
9+
* @tags security
10+
* experimental
11+
* external/cwe/cwe-409
12+
*/
13+
14+
import cpp
15+
import semmle.code.cpp.ir.dataflow.TaintTracking
16+
import semmle.code.cpp.security.FlowSources
17+
18+
/**
19+
* The `gzopen` function, which can perform command substitution.
20+
*/
21+
private class GzopenFunction extends Function {
22+
GzopenFunction() { hasGlobalName("gzopen") }
23+
}
24+
25+
/**
26+
* The `gzread` function, which can perform command substitution.
27+
*/
28+
private class GzreadFunction extends Function {
29+
GzreadFunction() { hasGlobalName("gzread") }
30+
}
31+
32+
module ZlibTaintConfig implements DataFlow::ConfigSig {
33+
predicate isSource(DataFlow::Node source) {
34+
exists(FunctionCall fc | fc.getTarget() instanceof GzopenFunction |
35+
fc.getArgument(0) = source.asExpr()
36+
)
37+
}
38+
39+
predicate isSink(DataFlow::Node sink) {
40+
exists(FunctionCall fc | fc.getTarget() instanceof GzreadFunction |
41+
fc.getArgument(0) = sink.asExpr() and
42+
not sanitizer(fc)
43+
)
44+
}
45+
46+
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
47+
exists(FunctionCall fc | fc.getTarget() instanceof GzopenFunction |
48+
node1.asExpr() = fc.getArgument(0) and
49+
node2.asExpr() = fc
50+
)
51+
}
52+
}
53+
54+
predicate sanitizer(FunctionCall fc) {
55+
exists(Expr e | fc.getTarget() instanceof GzreadFunction |
56+
// a RelationalOperation which isn't compared with a Literal that using for end of read
57+
TaintTracking::localExprTaint(fc, e.(RelationalOperation).getAChild*()) and
58+
not e.getAChild*().(Literal).getValue() = ["0", "1", "-1"]
59+
)
60+
}
61+
62+
module ZlibTaint = TaintTracking::Global<ZlibTaintConfig>;
63+
64+
import ZlibTaint::PathGraph
65+
66+
from ZlibTaint::PathNode source, ZlibTaint::PathNode sink
67+
where ZlibTaint::flowPath(source, sink)
68+
select sink.getNode(), source, sink, "This file extraction depends on a $@.", source.getNode(),
69+
"potentially untrusted source"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Extracting Compressed files with any compression algorithm like gzip can cause to denial of service attacks.</p>
7+
<p>Attackers can compress a huge file which created by repeated similiar byte and convert it to a small compressed file.</p>
8+
9+
</overview>
10+
<recommendation>
11+
12+
<p>When you want to decompress a user-provided compressed file you must be careful about the decompression ratio or read these files within a loop byte by byte to be able to manage the decompressed size in each cycle of the loop.</p>
13+
14+
</recommendation>
15+
<example>
16+
17+
<p>
18+
Reading uncompressed Gzip file within a loop and check for a threshold size in each cycle.
19+
</p>
20+
<sample src="example_good.cpp"/>
21+
22+
<p>
23+
An Unsafe Approach can be this example which we don't check for uncompressed size.
24+
</p>
25+
<sample src="example_bad.cpp" />
26+
27+
</example>
28+
<references>
29+
30+
<li>
31+
<a href="https://www.bamsoftware.com/hacks/zipbomb/">A great research to gain more impact by this kind of attacks</a>
32+
</li>
33+
34+
</references>
35+
</qhelp>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#include <iostream>
2+
#include <vector>
3+
#include "zlib.h"
4+
int UnsafeRead() {
5+
std::cout << "enter compressed file name!\n" << std::endl;
6+
char fileName[100];
7+
std::cin >> fileName;
8+
gzFile inFileZ = gzopen(fileName, "rb");
9+
if (inFileZ == nullptr) {
10+
printf("Error: Failed to gzopen %s\n", fileName);
11+
exit(0);
12+
}
13+
unsigned char unzipBuffer[8192];
14+
unsigned int unzippedBytes;
15+
std::vector<unsigned char> unzippedData;
16+
while (true) {
17+
unzippedBytes = gzread(inFileZ, unzipBuffer, 8192);
18+
if (unzippedBytes > 0) {
19+
unzippedData.insert(unzippedData.end(), unzipBuffer, unzipBuffer + unzippedBytes);
20+
} else {
21+
break;
22+
}
23+
}
24+
25+
for ( auto &&i: unzippedData)
26+
std::cout << i;
27+
gzclose(inFileZ);
28+
29+
return 0;
30+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#include <iostream>
2+
#include <vector>
3+
#include "zlib.h"
4+
int SafeRead() {
5+
std::cout << "enter compressed file name!\n" << std::endl;
6+
char fileName[100];
7+
std::cin >> fileName;
8+
gzFile inFileZ = gzopen(fileName, "rb");
9+
if (inFileZ == nullptr) {
10+
printf("Error: Failed to gzopen %s\n", fileName);
11+
exit(0);
12+
}
13+
unsigned char unzipBuffer[8192];
14+
unsigned int unzippedBytes;
15+
uint totalRead = 0;
16+
std::vector<unsigned char> unzippedData;
17+
while (true) {
18+
unzippedBytes = gzread(inFileZ, unzipBuffer, 8192);
19+
totalRead += unzippedBytes;
20+
if (unzippedBytes > 0) {
21+
unzippedData.insert(unzippedData.end(), unzipBuffer, unzipBuffer + unzippedBytes);
22+
if (totalRead > 1024 * 1024 * 4) {
23+
std::cout << "Bombs!" << totalRead;
24+
exit(1);
25+
} else {
26+
std::cout << "not Bomb yet!!" << totalRead << std::endl;
27+
}
28+
} else {
29+
break;
30+
}
31+
}
32+
33+
for (auto &&i: unzippedData)
34+
std::cout << i;
35+
gzclose(inFileZ);
36+
37+
return 0;
38+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* @name User-controlled file decompression
3+
* @description User-controlled data that flows into decompression library APIs without checking the compression rate is dangerous
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 7.8
7+
* @precision medium
8+
* @id cs/user-controlled-file-decompression
9+
* @tags security
10+
* experimental
11+
* external/cwe/cwe-409
12+
*/
13+
14+
import csharp
15+
import semmle.code.csharp.security.dataflow.flowsources.Remote
16+
17+
/**
18+
* A data flow source for unsafe Decompression extraction.
19+
*/
20+
abstract class DecompressionSource extends DataFlow::Node { }
21+
22+
class ZipOpenReadSource extends DecompressionSource {
23+
ZipOpenReadSource() {
24+
exists(MethodCall mc |
25+
mc.getTarget().hasQualifiedName("System.IO.Compression", "ZipFile", ["OpenRead", "Open"]) and
26+
this.asExpr() = mc.getArgument(0) and
27+
not mc.getArgument(0).getType().isConst()
28+
)
29+
}
30+
}
31+
32+
/** A path argument to a call to the `ZipArchive` constructor call. */
33+
class ZipArchiveArgSource extends DecompressionSource {
34+
ZipArchiveArgSource() {
35+
exists(ObjectCreation oc |
36+
oc.getTarget().getDeclaringType().hasQualifiedName("System.IO.Compression", "ZipArchive")
37+
|
38+
this.asExpr() = oc.getArgument(0)
39+
)
40+
}
41+
}
42+
43+
/**
44+
* A data flow sink for unsafe zip extraction.
45+
*/
46+
abstract class DecompressionSink extends DataFlow::Node { }
47+
48+
/** A Caller of the `ExtractToFile` method. */
49+
class ExtractToFileCallSink extends DecompressionSink {
50+
ExtractToFileCallSink() {
51+
exists(MethodCall mc |
52+
mc.getTarget().hasQualifiedName("System.IO.Compression", "ZipFileExtensions", "ExtractToFile") and
53+
this.asExpr() = mc.getArgumentForName("source")
54+
)
55+
}
56+
}
57+
58+
/** A Qualifier of the `Open()` method. */
59+
class OpenCallSink extends DecompressionSink {
60+
OpenCallSink() {
61+
exists(MethodCall mc |
62+
mc.getTarget().hasQualifiedName("System.IO.Compression", "ZipArchiveEntry", "Open") and
63+
this.asExpr() = mc.getQualifier()
64+
)
65+
}
66+
}
67+
68+
/** A Call to the `GZipStreamSink` first arugument of Constructor Call . */
69+
class GZipStreamSink extends DecompressionSink, DecompressionSource {
70+
GZipStreamSink() {
71+
exists(Constructor mc |
72+
mc.getDeclaringType().hasQualifiedName("System.IO.Compression", "GZipStream") and
73+
this.asExpr() = mc.getACall().getArgument(0)
74+
)
75+
}
76+
}
77+
78+
/**
79+
* A taint tracking configuration for Decompression Bomb.
80+
*/
81+
private module DecompressionBombConfig implements DataFlow::ConfigSig {
82+
predicate isSource(DataFlow::Node source) {
83+
source instanceof DecompressionSource
84+
or
85+
source instanceof RemoteFlowSource
86+
}
87+
88+
predicate isSink(DataFlow::Node sink) { sink instanceof DecompressionSink }
89+
90+
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
91+
// var node2 = new ZipArchive(node1, ZipArchiveMode.Read);
92+
exists(ObjectCreation oc |
93+
oc.getTarget().getDeclaringType().hasQualifiedName("System.IO.Compression", "ZipArchive") and
94+
node2.asExpr() = oc and
95+
node1.asExpr() = oc.getArgumentForName("stream")
96+
)
97+
or
98+
// var node2 = node1.ExtractToFile("./output.txt", true)
99+
exists(MethodCall mc |
100+
mc.getTarget().hasQualifiedName("System.IO.Compression", "ZipFileExtensions", "ExtractToFile") and
101+
node2.asExpr() = mc and
102+
node1.asExpr() = mc.getArgumentForName("source")
103+
)
104+
or
105+
// var node2 = node1.OpenReadStream()
106+
exists(MethodCall mc |
107+
mc.getTarget().hasQualifiedName("Microsoft.AspNetCore.Http", "IFormFile", "OpenReadStream") and
108+
node2.asExpr() = mc and
109+
node1.asExpr() = mc.getQualifier()
110+
)
111+
}
112+
}
113+
114+
module DecompressionBomb = TaintTracking::Global<DecompressionBombConfig>;
115+
116+
import DecompressionBomb::PathGraph
117+
118+
from DecompressionBomb::PathNode source, DecompressionBomb::PathNode sink
119+
where DecompressionBomb::flowPath(source, sink)
120+
select sink.getNode(), source, sink, "This file extraction depends on a $@.", source.getNode(),
121+
"potentially untrusted source"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Extracting Compressed files with any compression algorithm like gzip can cause to denial of service attacks.</p>
7+
<p>Attackers can compress a huge file which created by repeated similiar byte and convert it to a small compressed file.</p>
8+
9+
</overview>
10+
<recommendation>
11+
12+
<p>When you want to decompress a user-provided compressed file you must be careful about the decompression ratio or read these files within a loop byte by byte to be able to manage the decompressed size in each cycle of the loop.</p>
13+
14+
</recommendation>
15+
<example>
16+
17+
<p>A good Blog Post about decompression bombs and recommended method is already written by Gérald Barré in <a href="https://www.meziantou.net/prevent-zip-bombs-in-dotnet.htm">this</a> blog post</p>
18+
19+
<references>
20+
21+
<li>
22+
<a href="https://www.bamsoftware.com/hacks/zipbomb/">A great research to gain more impact by this kind of attack</a>
23+
</li>
24+
25+
</references>
26+
</qhelp>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import csharp
2+
import semmle.code.csharp.security.dataflow.flowsources.Remote
3+
4+
/** A data flow source of remote user input by Form File (ASP.NET unvalidated request data). */
5+
class FormFile extends AspNetRemoteFlowSource {
6+
FormFile() {
7+
exists(MethodCall mc |
8+
mc.getTarget()
9+
.hasQualifiedName(["Microsoft.AspNetCore.Http", "Microsoft.AspNetCore.Http.Features"],
10+
"IFormFile", ["OpenReadStream", "ContentType", "ContentDisposition", "Name", "FileName"]) and
11+
this.asExpr() = mc
12+
)
13+
or
14+
exists(MethodCall mc |
15+
mc.getTarget()
16+
.hasQualifiedName(["Microsoft.AspNetCore.Http", "Microsoft.AspNetCore.Http.Features"],
17+
"IFormFile", "CopyTo") and
18+
this.asParameter() = mc.getTarget().getParameter(0)
19+
)
20+
or
21+
exists(Property fa |
22+
fa.getDeclaringType()
23+
.hasQualifiedName(["Microsoft.AspNetCore.Http", "Microsoft.AspNetCore.Http.Features"],
24+
"IFormFile") and
25+
fa.hasName(["ContentType", "ContentDisposition", "Name", "FileName"]) and
26+
this.asExpr() = fa.getAnAccess()
27+
)
28+
}
29+
30+
override string getSourceType() {
31+
result = "ASP.NET unvalidated request data from multipart request"
32+
}
33+
}
34+
35+
/** A data flow source of remote user input by Form (ASP.NET unvalidated request data). */
36+
class FormCollection extends AspNetRemoteFlowSource {
37+
FormCollection() {
38+
exists(Property fa |
39+
fa.getDeclaringType().hasQualifiedName("Microsoft.AspNetCore.Http", "IFormCollection") and
40+
fa.hasName("Keys") and
41+
this.asExpr() = fa.getAnAccess()
42+
)
43+
}
44+
45+
override string getSourceType() {
46+
result = "ASP.NET unvalidated request data from multipart request Form Keys"
47+
}
48+
}
49+
50+
/** A data flow source of remote user input by Headers (ASP.NET unvalidated request data). */
51+
class HeaderDictionary extends AspNetRemoteFlowSource {
52+
HeaderDictionary() {
53+
exists(Property fa |
54+
fa.getDeclaringType().hasQualifiedName("Microsoft.AspNetCore.Http", "IHeaderDictionary") and
55+
this.asExpr() = fa.getAnAccess()
56+
)
57+
}
58+
59+
override string getSourceType() {
60+
result = "ASP.NET unvalidated request data from Headers of request"
61+
}
62+
}

0 commit comments

Comments
 (0)