Skip to content

Commit adc1a30

Browse files
authored
Merge pull request #9432 from thiggy1342/experimental-decompression-api
RB: Adding decompression-api to experimental ruleset
2 parents 875776d + c5bf1b8 commit adc1a30

File tree

6 files changed

+131
-0
lines changed

6 files changed

+131
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
Decompression of user-controlled data without taking proper precaution can
8+
result in uncontrolled and massive decompression on the server, resulting
9+
in a denial of service.
10+
</p>
11+
</overview>
12+
<recommendation>
13+
<p>
14+
When decompressing files supplied by the user, make sure that you're checking
15+
the size of the incoming data chunks before writing to an output.
16+
</p>
17+
</recommendation>
18+
<example>
19+
<p>
20+
In this example, the size of the input buffer chunks and total size are checked before each chunk is written to the output.
21+
</p>
22+
<sample src="examples/decompress.rb" />
23+
</example>
24+
25+
<references>
26+
</references>
27+
</qhelp>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* @name User-controlled file decompression
3+
* @description User-controlled data that flows into decompression library APIs could be dangerous
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 7.8
7+
* @precision medium
8+
* @id rb/user-controlled-file-decompression
9+
* @tags security external/cwe/cwe-409
10+
*/
11+
12+
import ruby
13+
import codeql.ruby.ApiGraphs
14+
import codeql.ruby.DataFlow
15+
import codeql.ruby.dataflow.RemoteFlowSources
16+
import codeql.ruby.TaintTracking
17+
import DataFlow::PathGraph
18+
19+
class DecompressionApiUse extends DataFlow::Node {
20+
private DataFlow::CallNode call;
21+
22+
// this should find the first argument in calls to Zlib::Inflate.inflate or Zip::File.open_buffer
23+
DecompressionApiUse() {
24+
this = call.getArgument(0) and
25+
(
26+
call = API::getTopLevelMember("Zlib").getMember("Inflate").getAMethodCall("inflate") or
27+
call = API::getTopLevelMember("Zip").getMember("File").getAMethodCall("open_buffer")
28+
)
29+
}
30+
31+
// returns calls to Zlib::Inflate.inflate or Zip::File.open_buffer
32+
DataFlow::CallNode getCall() { result = call }
33+
}
34+
35+
class Configuration extends TaintTracking::Configuration {
36+
Configuration() { this = "DecompressionApiUse" }
37+
38+
// this predicate will be used to constrain our query to find instances where only remote user-controlled data flows to the sink
39+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
40+
41+
// our Decompression APIs defined above will be the sinks we use for this query
42+
override predicate isSink(DataFlow::Node sink) { sink instanceof DecompressionApiUse }
43+
}
44+
45+
from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
46+
where config.hasFlowPath(source, sink)
47+
select sink.getNode().(DecompressionApiUse), source, sink,
48+
"This call to $@ is unsafe because user-controlled data is used to set the object being decompressed, which could lead to a denial of service attack or malicious code extracted from an unknown source.",
49+
sink.getNode().(DecompressionApiUse).getCall(),
50+
sink.getNode().(DecompressionApiUse).getCall().getMethodName()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class UsersController < ActionController::Base
2+
def example_zlib_inflate
3+
MAX_ALLOWED_CHUNK_SIZE = 256
4+
MAX_ALLOWED_TOTAL_SIZE = 1024
5+
6+
user_data = params[:data]
7+
output = []
8+
outsize = 0
9+
10+
Zlib::Inflate.inflate(user_data) { |chunk|
11+
outsize += chunk.size
12+
if chunk.size < MAX_ALLOWED_CHUNK_SIZE && outsize < MAX_ALLOWED_TOTAL_SIZE
13+
output << chunk
14+
end
15+
}
16+
end
17+
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
edges
2+
| decompression_api.rb:4:16:4:21 | call to params : | decompression_api.rb:4:16:4:28 | ...[...] : |
3+
| decompression_api.rb:4:16:4:28 | ...[...] : | decompression_api.rb:5:31:5:34 | path |
4+
| decompression_api.rb:15:31:15:36 | call to params : | decompression_api.rb:15:31:15:43 | ...[...] |
5+
nodes
6+
| decompression_api.rb:4:16:4:21 | call to params : | semmle.label | call to params : |
7+
| decompression_api.rb:4:16:4:28 | ...[...] : | semmle.label | ...[...] : |
8+
| decompression_api.rb:5:31:5:34 | path | semmle.label | path |
9+
| decompression_api.rb:15:31:15:36 | call to params : | semmle.label | call to params : |
10+
| decompression_api.rb:15:31:15:43 | ...[...] | semmle.label | ...[...] |
11+
subpaths
12+
#select
13+
| decompression_api.rb:5:31:5:34 | path | decompression_api.rb:4:16:4:21 | call to params : | decompression_api.rb:5:31:5:34 | path | This call to $@ is unsafe because user-controlled data is used to set the object being decompressed, which could lead to a denial of service attack or malicious code extracted from an unknown source. | decompression_api.rb:5:9:5:35 | call to inflate | inflate |
14+
| decompression_api.rb:15:31:15:43 | ...[...] | decompression_api.rb:15:31:15:36 | call to params : | decompression_api.rb:15:31:15:43 | ...[...] | This call to $@ is unsafe because user-controlled data is used to set the object being decompressed, which could lead to a denial of service attack or malicious code extracted from an unknown source. | decompression_api.rb:15:9:15:44 | call to open_buffer | open_buffer |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/decompression-api/DecompressionApi.ql
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class TestController < ActionController::Base
2+
# this should get picked up
3+
def unsafe_zlib_unzip
4+
path = params[:file]
5+
Zlib::Inflate.inflate(path)
6+
end
7+
8+
# this should not get picked up
9+
def safe_zlib_unzip
10+
Zlib::Inflate.inflate(file)
11+
end
12+
13+
# this should get picked up
14+
def unsafe_zlib_unzip
15+
Zip::File.open_buffer(params[:file])
16+
end
17+
18+
# this should not get picked up
19+
def safe_zlib_unzip
20+
Zip::File.open_buffer(file)
21+
end
22+
end

0 commit comments

Comments
 (0)