Skip to content

Commit 5157d4d

Browse files
authored
Merge pull request github#11581 from erik-krogh/stdin
Rb: add stdin as source for unsafe-deserialization
2 parents e9bbb5d + 0a17696 commit 5157d4d

File tree

9 files changed

+98
-72
lines changed

9 files changed

+98
-72
lines changed

ruby/ql/lib/codeql/ruby/frameworks/Core.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import core.String
1616
import core.Regexp
1717
import core.IO
1818
import core.Digest
19+
import core.Base64
1920

2021
/**
2122
* A system command executed via subshell literal syntax.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Provides modeling for the `Base64` module.
3+
*/
4+
5+
private import ruby
6+
private import codeql.ruby.dataflow.FlowSummary
7+
private import codeql.ruby.ApiGraphs
8+
9+
private class Base64Decode extends SummarizedCallable {
10+
Base64Decode() { this = "Base64.decode64()" }
11+
12+
override MethodCall getACall() {
13+
result =
14+
API::getTopLevelMember("Base64")
15+
.getAMethodCall(["decode64", "strict_decode64", "urlsafe_decode64"])
16+
.asExpr()
17+
.getExpr()
18+
}
19+
20+
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
21+
input = "Argument[0]" and
22+
output = "ReturnValue" and
23+
preservesValue = false
24+
}
25+
}

ruby/ql/lib/codeql/ruby/security/UnsafeDeserializationCustomizations.qll

Lines changed: 35 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ private import codeql.ruby.DataFlow
1010
private import codeql.ruby.dataflow.RemoteFlowSources
1111
private import codeql.ruby.frameworks.ActiveJob
1212
private import codeql.ruby.frameworks.core.Module
13+
private import codeql.ruby.frameworks.core.Kernel
1314

1415
module UnsafeDeserialization {
1516
/**
1617
* A data flow source for unsafe deserialization vulnerabilities.
1718
*/
18-
abstract class Source extends DataFlow::Node { }
19+
abstract class Source extends DataFlow::Node {
20+
/** Gets a string that describes the source. */
21+
string describe() { result = "user-provided value" }
22+
}
1923

2024
/**
2125
* A data flow sink for unsafe deserialization vulnerabilities.
@@ -27,16 +31,39 @@ module UnsafeDeserialization {
2731
*/
2832
abstract class Sanitizer extends DataFlow::Node { }
2933

30-
/**
31-
* Additional taint steps for "unsafe deserialization" vulnerabilities.
32-
*/
33-
predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
34-
base64DecodeTaintStep(fromNode, toNode)
35-
}
36-
3734
/** A source of remote user input, considered as a flow source for unsafe deserialization. */
3835
class RemoteFlowSourceAsSource extends Source instanceof RemoteFlowSource { }
3936

37+
/** A read of data from `STDIN`/`ARGV`, considered as a flow source for unsafe deserialization. */
38+
class StdInSource extends UnsafeDeserialization::Source {
39+
boolean stdin;
40+
41+
StdInSource() {
42+
this = API::getTopLevelMember(["STDIN", "ARGF"]).getAMethodCall(["gets", "read"]) and
43+
stdin = true
44+
or
45+
// > $stdin == STDIN
46+
// => true
47+
// but $stdin is special in that it is a global variable and not a constant. `API::getTopLevelMember` only gets constants.
48+
exists(DataFlow::Node dollarStdin |
49+
dollarStdin.asExpr().getExpr().(GlobalVariableReadAccess).getVariable().getName() = "$stdin" and
50+
this = dollarStdin.getALocalSource().getAMethodCall(["gets", "read"])
51+
) and
52+
stdin = true
53+
or
54+
// ARGV.
55+
this.asExpr().getExpr().(GlobalVariableReadAccess).getVariable().getName() = "ARGV" and
56+
stdin = false
57+
or
58+
this.(Kernel::KernelMethodCall).getMethodName() = ["gets", "readline", "readlines"] and
59+
stdin = true
60+
}
61+
62+
override string describe() {
63+
if stdin = true then result = "value from stdin" else result = "value from ARGV"
64+
}
65+
}
66+
4067
/**
4168
* An argument in a call to `Marshal.load` or `Marshal.restore`, considered a
4269
* sink for unsafe deserialization.
@@ -181,41 +208,4 @@ module UnsafeDeserialization {
181208
)
182209
}
183210
}
184-
185-
/**
186-
* `Base64.decode64` propagates taint from its argument to its return value.
187-
*/
188-
predicate base64DecodeTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
189-
exists(DataFlow::CallNode callNode |
190-
callNode =
191-
API::getTopLevelMember("Base64")
192-
.getAMethodCall(["decode64", "strict_decode64", "urlsafe_decode64"])
193-
|
194-
fromNode = callNode.getArgument(0) and
195-
toNode = callNode
196-
)
197-
}
198-
199-
/**
200-
* A argument in a call to `Module.const_get`, considered as a sink for unsafe
201-
* deserialization.
202-
*
203-
* Calls to `Module.const_get` can return arbitrary classes which can then be
204-
* instantiated.
205-
*/
206-
class ConstGetCallArgument extends Sink {
207-
ConstGetCallArgument() { this = any(Module::ModuleConstGetCallCodeExecution c).getCode() }
208-
}
209-
210-
/**
211-
* A argument in a call to `ActiveJob::Serializers.deserialize`, considered as
212-
* a sink for unsafe deserialization.
213-
*
214-
* This is roughly equivalent to a call to `Module.const_get`.
215-
*/
216-
class ActiveJobSerializersDeserializeArgument extends Sink {
217-
ActiveJobSerializersDeserializeArgument() {
218-
this = any(ActiveJob::Serializers::DeserializeCall c).getCode()
219-
}
220-
}
221211
}

ruby/ql/lib/codeql/ruby/security/UnsafeDeserializationQuery.qll

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,4 @@ class Configuration extends TaintTracking::Configuration {
2727
super.isSanitizer(node) or
2828
node instanceof UnsafeDeserialization::Sanitizer
2929
}
30-
31-
override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
32-
UnsafeDeserialization::isAdditionalTaintStep(fromNode, toNode)
33-
}
3430
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* The `rb/unsafe-deserialization` query now recognizes input from STDIN as a source.

ruby/ql/src/queries/security/cwe-502/UnsafeDeserialization.ql

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
* external/cwe/cwe-502
1212
*/
1313

14-
import codeql.ruby.AST
15-
import DataFlow::PathGraph
16-
import codeql.ruby.DataFlow
14+
import ruby
1715
import codeql.ruby.security.UnsafeDeserializationQuery
16+
import DataFlow::PathGraph
1817

1918
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
2019
where cfg.hasFlowPath(source, sink)
2120
select sink.getNode(), source, sink, "Unsafe deserialization depends on a $@.", source.getNode(),
22-
"user-provided value"
21+
source.getNode().(UnsafeDeserialization::Source).describe()

ruby/ql/test/library-tests/dataflow/local/TaintStep.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2804,6 +2804,7 @@
28042804
| file://:0:0:0:0 | parameter position 0 of ActionController::Parameters#merge! | file://:0:0:0:0 | [summary] to write: argument self in ActionController::Parameters#merge! |
28052805
| file://:0:0:0:0 | parameter position 0 of ActionController::Parameters#merge! | file://:0:0:0:0 | [summary] to write: return (return) in ActionController::Parameters#merge! |
28062806
| file://:0:0:0:0 | parameter position 0 of Arel.sql | file://:0:0:0:0 | [summary] to write: return (return) in Arel.sql |
2807+
| file://:0:0:0:0 | parameter position 0 of Base64.decode64() | file://:0:0:0:0 | [summary] to write: return (return) in Base64.decode64() |
28072808
| file://:0:0:0:0 | parameter position 0 of File.absolute_path | file://:0:0:0:0 | [summary] to write: return (return) in File.absolute_path |
28082809
| file://:0:0:0:0 | parameter position 0 of File.dirname | file://:0:0:0:0 | [summary] to write: return (return) in File.dirname |
28092810
| file://:0:0:0:0 | parameter position 0 of File.expand_path | file://:0:0:0:0 | [summary] to write: return (return) in File.expand_path |

ruby/ql/test/query-tests/security/cwe-502/unsafe-deserialization/UnsafeDeserialization.expected

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
edges
2+
| UnsafeDeserialization.rb:10:23:10:50 | call to decode64 : | UnsafeDeserialization.rb:11:27:11:41 | serialized_data |
23
| UnsafeDeserialization.rb:10:39:10:44 | call to params : | UnsafeDeserialization.rb:10:39:10:50 | ...[...] : |
3-
| UnsafeDeserialization.rb:10:39:10:50 | ...[...] : | UnsafeDeserialization.rb:11:27:11:41 | serialized_data |
4+
| UnsafeDeserialization.rb:10:39:10:50 | ...[...] : | UnsafeDeserialization.rb:10:23:10:50 | call to decode64 : |
5+
| UnsafeDeserialization.rb:16:23:16:50 | call to decode64 : | UnsafeDeserialization.rb:17:30:17:44 | serialized_data |
46
| UnsafeDeserialization.rb:16:39:16:44 | call to params : | UnsafeDeserialization.rb:16:39:16:50 | ...[...] : |
5-
| UnsafeDeserialization.rb:16:39:16:50 | ...[...] : | UnsafeDeserialization.rb:17:30:17:44 | serialized_data |
7+
| UnsafeDeserialization.rb:16:39:16:50 | ...[...] : | UnsafeDeserialization.rb:16:23:16:50 | call to decode64 : |
68
| UnsafeDeserialization.rb:22:17:22:22 | call to params : | UnsafeDeserialization.rb:22:17:22:28 | ...[...] : |
79
| UnsafeDeserialization.rb:22:17:22:28 | ...[...] : | UnsafeDeserialization.rb:23:24:23:32 | json_data |
810
| UnsafeDeserialization.rb:28:17:28:22 | call to params : | UnsafeDeserialization.rb:28:17:28:28 | ...[...] : |
@@ -18,12 +20,12 @@ edges
1820
| UnsafeDeserialization.rb:81:11:81:22 | ...[...] : | UnsafeDeserialization.rb:82:34:82:36 | xml |
1921
| UnsafeDeserialization.rb:87:17:87:22 | call to params : | UnsafeDeserialization.rb:87:17:87:28 | ...[...] : |
2022
| UnsafeDeserialization.rb:87:17:87:28 | ...[...] : | UnsafeDeserialization.rb:88:25:88:33 | yaml_data |
21-
| UnsafeDeserialization.rb:93:30:93:35 | call to params : | UnsafeDeserialization.rb:93:30:93:43 | ...[...] |
22-
| UnsafeDeserialization.rb:99:48:99:53 | call to params : | UnsafeDeserialization.rb:99:48:99:61 | ...[...] |
2323
nodes
24+
| UnsafeDeserialization.rb:10:23:10:50 | call to decode64 : | semmle.label | call to decode64 : |
2425
| UnsafeDeserialization.rb:10:39:10:44 | call to params : | semmle.label | call to params : |
2526
| UnsafeDeserialization.rb:10:39:10:50 | ...[...] : | semmle.label | ...[...] : |
2627
| UnsafeDeserialization.rb:11:27:11:41 | serialized_data | semmle.label | serialized_data |
28+
| UnsafeDeserialization.rb:16:23:16:50 | call to decode64 : | semmle.label | call to decode64 : |
2729
| UnsafeDeserialization.rb:16:39:16:44 | call to params : | semmle.label | call to params : |
2830
| UnsafeDeserialization.rb:16:39:16:50 | ...[...] : | semmle.label | ...[...] : |
2931
| UnsafeDeserialization.rb:17:30:17:44 | serialized_data | semmle.label | serialized_data |
@@ -49,10 +51,11 @@ nodes
4951
| UnsafeDeserialization.rb:87:17:87:22 | call to params : | semmle.label | call to params : |
5052
| UnsafeDeserialization.rb:87:17:87:28 | ...[...] : | semmle.label | ...[...] : |
5153
| UnsafeDeserialization.rb:88:25:88:33 | yaml_data | semmle.label | yaml_data |
52-
| UnsafeDeserialization.rb:93:30:93:35 | call to params : | semmle.label | call to params : |
53-
| UnsafeDeserialization.rb:93:30:93:43 | ...[...] | semmle.label | ...[...] |
54-
| UnsafeDeserialization.rb:99:48:99:53 | call to params : | semmle.label | call to params : |
55-
| UnsafeDeserialization.rb:99:48:99:61 | ...[...] | semmle.label | ...[...] |
54+
| UnsafeDeserialization.rb:92:24:92:34 | call to read | semmle.label | call to read |
55+
| UnsafeDeserialization.rb:95:24:95:33 | call to gets | semmle.label | call to gets |
56+
| UnsafeDeserialization.rb:98:24:98:32 | call to read | semmle.label | call to read |
57+
| UnsafeDeserialization.rb:101:24:101:27 | call to gets | semmle.label | call to gets |
58+
| UnsafeDeserialization.rb:104:24:104:32 | call to readlines | semmle.label | call to readlines |
5659
subpaths
5760
#select
5861
| UnsafeDeserialization.rb:11:27:11:41 | serialized_data | UnsafeDeserialization.rb:10:39:10:44 | call to params : | UnsafeDeserialization.rb:11:27:11:41 | serialized_data | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:10:39:10:44 | call to params | user-provided value |
@@ -65,5 +68,8 @@ subpaths
6568
| UnsafeDeserialization.rb:69:23:69:31 | json_data | UnsafeDeserialization.rb:59:17:59:22 | call to params : | UnsafeDeserialization.rb:69:23:69:31 | json_data | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:59:17:59:22 | call to params | user-provided value |
6669
| UnsafeDeserialization.rb:82:34:82:36 | xml | UnsafeDeserialization.rb:81:11:81:16 | call to params : | UnsafeDeserialization.rb:82:34:82:36 | xml | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:81:11:81:16 | call to params | user-provided value |
6770
| UnsafeDeserialization.rb:88:25:88:33 | yaml_data | UnsafeDeserialization.rb:87:17:87:22 | call to params : | UnsafeDeserialization.rb:88:25:88:33 | yaml_data | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:87:17:87:22 | call to params | user-provided value |
68-
| UnsafeDeserialization.rb:93:30:93:43 | ...[...] | UnsafeDeserialization.rb:93:30:93:35 | call to params : | UnsafeDeserialization.rb:93:30:93:43 | ...[...] | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:93:30:93:35 | call to params | user-provided value |
69-
| UnsafeDeserialization.rb:99:48:99:61 | ...[...] | UnsafeDeserialization.rb:99:48:99:53 | call to params : | UnsafeDeserialization.rb:99:48:99:61 | ...[...] | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:99:48:99:53 | call to params | user-provided value |
71+
| UnsafeDeserialization.rb:92:24:92:34 | call to read | UnsafeDeserialization.rb:92:24:92:34 | call to read | UnsafeDeserialization.rb:92:24:92:34 | call to read | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:92:24:92:34 | call to read | value from stdin |
72+
| UnsafeDeserialization.rb:95:24:95:33 | call to gets | UnsafeDeserialization.rb:95:24:95:33 | call to gets | UnsafeDeserialization.rb:95:24:95:33 | call to gets | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:95:24:95:33 | call to gets | value from stdin |
73+
| UnsafeDeserialization.rb:98:24:98:32 | call to read | UnsafeDeserialization.rb:98:24:98:32 | call to read | UnsafeDeserialization.rb:98:24:98:32 | call to read | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:98:24:98:32 | call to read | value from stdin |
74+
| UnsafeDeserialization.rb:101:24:101:27 | call to gets | UnsafeDeserialization.rb:101:24:101:27 | call to gets | UnsafeDeserialization.rb:101:24:101:27 | call to gets | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:101:24:101:27 | call to gets | value from stdin |
75+
| UnsafeDeserialization.rb:104:24:104:32 | call to readlines | UnsafeDeserialization.rb:104:24:104:32 | call to readlines | UnsafeDeserialization.rb:104:24:104:32 | call to readlines | Unsafe deserialization depends on a $@. | UnsafeDeserialization.rb:104:24:104:32 | call to readlines | value from stdin |

ruby/ql/test/query-tests/security/cwe-502/unsafe-deserialization/UnsafeDeserialization.rb

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,19 @@ def route11
8888
object = Psych.load yaml_data
8989
end
9090

91-
# BAD - user input determines which class is instantiated
92-
def route12
93-
klass = Module.const_get(params[:class])
94-
object = klass.new
95-
end
91+
def stdin
92+
object = YAML.load $stdin.read
93+
94+
# STDIN
95+
object = YAML.load STDIN.gets
96+
97+
# ARGF
98+
object = YAML.load ARGF.read
99+
100+
# Kernel.gets
101+
object = YAML.load gets
96102

97-
# BAD - user input determines which class is instantiated
98-
def route13
99-
klass = ActiveJob::Serializers.deserialize(params[:class])
100-
object = klass.new
103+
# Kernel.readlines
104+
object = YAML.load readlines
101105
end
102106
end

0 commit comments

Comments
 (0)