Skip to content

Commit ecc9f07

Browse files
authored
Merge pull request #311 from github/nickrolfe/oj
Consider Oj.load a sink for unsafe deserialization
2 parents 9640af0 + 8e14b65 commit ecc9f07

13 files changed

+269
-76
lines changed

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,113 @@ module UnsafeDeserialization {
6767
}
6868
}
6969

70+
private string getAKnownOjModeName(boolean isSafe) {
71+
result = ["compat", "custom", "json", "null", "rails", "strict", "wab"] and isSafe = true
72+
or
73+
result = "object" and isSafe = false
74+
}
75+
76+
private predicate isOjModePair(Pair p, string modeValue) {
77+
p.getKey().getValueText() = "mode" and
78+
exists(DataFlow::LocalSourceNode symbolLiteral, DataFlow::Node value |
79+
symbolLiteral.asExpr().getExpr().(SymbolLiteral).getValueText() = modeValue and
80+
symbolLiteral.flowsTo(value) and
81+
value.asExpr().getExpr() = p.getValue()
82+
)
83+
}
84+
85+
/**
86+
* A node representing a hash that contains the key `:mode`.
87+
*/
88+
private class OjOptionsHashWithModeKey extends DataFlow::Node {
89+
private string modeValue;
90+
91+
OjOptionsHashWithModeKey() {
92+
exists(DataFlow::LocalSourceNode options |
93+
options.flowsTo(this) and
94+
isOjModePair(options.asExpr().getExpr().(HashLiteral).getAKeyValuePair(), modeValue)
95+
)
96+
}
97+
98+
/**
99+
* Holds if this hash node contains a `:mode` key whose value is one known
100+
* to be `isSafe` with untrusted data.
101+
*/
102+
predicate hasKnownMode(boolean isSafe) { modeValue = getAKnownOjModeName(isSafe) }
103+
104+
/**
105+
* Holds if this hash node contains a `:mode` key whose value is one of the
106+
* `Oj` modes known to be safe to use with untrusted data.
107+
*/
108+
predicate hasSafeMode() { this.hasKnownMode(true) }
109+
}
110+
111+
/**
112+
* A call node that sets `Oj.default_options`.
113+
*
114+
* ```rb
115+
* Oj.default_options = { allow_blank: true, mode: :compat }
116+
* ```
117+
*/
118+
private class SetOjDefaultOptionsCall extends DataFlow::CallNode {
119+
SetOjDefaultOptionsCall() {
120+
this = API::getTopLevelMember("Oj").getAMethodCall("default_options=")
121+
}
122+
123+
/**
124+
* Gets the value being assigned to `Oj.default_options`.
125+
*/
126+
DataFlow::Node getValue() {
127+
result.asExpr() =
128+
this.getArgument(0).asExpr().(CfgNodes::ExprNodes::AssignExprCfgNode).getRhs()
129+
}
130+
}
131+
132+
/**
133+
* A call to `Oj.load`.
134+
*/
135+
private class OjLoadCall extends DataFlow::CallNode {
136+
OjLoadCall() { this = API::getTopLevelMember("Oj").getAMethodCall("load") }
137+
138+
/**
139+
* Holds if this call to `Oj.load` includes an explicit options hash
140+
* argument that sets the mode to one that is known to be `isSafe`.
141+
*/
142+
predicate hasExplicitKnownMode(boolean isSafe) {
143+
exists(DataFlow::Node arg, int i | i >= 1 and arg = this.getArgument(i) |
144+
arg.(OjOptionsHashWithModeKey).hasKnownMode(isSafe)
145+
or
146+
isOjModePair(arg.asExpr().getExpr(), getAKnownOjModeName(isSafe))
147+
)
148+
}
149+
}
150+
151+
/**
152+
* An argument in a call to `Oj.load` where the mode is `:object` (which is
153+
* the default), considered a sink for unsafe deserialization.
154+
*/
155+
class UnsafeOjLoadArgument extends Sink {
156+
UnsafeOjLoadArgument() {
157+
exists(OjLoadCall ojLoad |
158+
this = ojLoad.getArgument(0) and
159+
// Exclude calls that explicitly pass a safe mode option.
160+
not ojLoad.hasExplicitKnownMode(true) and
161+
(
162+
// Sinks to include:
163+
// - Calls with an explicit, unsafe mode option.
164+
ojLoad.hasExplicitKnownMode(false)
165+
or
166+
// - Calls with no explicit mode option, unless there exists a call
167+
// anywhere to set the default options to a known safe mode.
168+
not ojLoad.hasExplicitKnownMode(_) and
169+
not exists(SetOjDefaultOptionsCall setOpts |
170+
setOpts.getValue().(OjOptionsHashWithModeKey).hasSafeMode()
171+
)
172+
)
173+
)
174+
}
175+
}
176+
70177
/**
71178
* `Base64.decode64` propagates taint from its argument to its return value.
72179
*/

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@ deserialization of arbitrary objects.
2121

2222
<example>
2323
<p>
24-
The following example calls the <code>Marshal.load</code>, <code>JSON.load</code>, and
25-
<code>YAML.load</code> methods on data from an HTTP request. Since these methods
26-
are capable of deserializing to arbitrary objects, this is inherently unsafe.
24+
The following example calls the <code>Marshal.load</code>,
25+
<code>JSON.load</code>, <code>YAML.load</code>, and <code>Oj.load</code> methods
26+
on data from an HTTP request. Since these methods are capable of deserializing
27+
to arbitrary objects, this is inherently unsafe.
2728
</p>
2829
<sample src="examples/UnsafeDeserializationBad.rb"/>
2930
<p>
3031
Using <code>JSON.parse</code> and <code>YAML.safe_load</code> instead, as in the
31-
following example, removes the vulnerability. Note that there is no safe way to
32-
deserialize untrusted data using <code>Marshal</code>.
32+
following example, removes the vulnerability. Similarly, calling
33+
<code>Oj.load</code> with any mode other than <code>:object</code> is safe, as
34+
is calling <code>Oj.safe_load</code>. Note that there is no safe way to deserialize
35+
untrusted data using <code>Marshal</code>.
3336
</p>
3437
<sample src="examples/UnsafeDeserializationGood.rb"/>
3538
</example>

ql/src/queries/security/cwe-502/examples/UnsafeDeserializationBad.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'json'
22
require 'yaml'
3+
require 'oj'
34

45
class UserController < ActionController::Base
56
def marshal_example
@@ -17,4 +18,9 @@ def yaml_example
1718
object = YAML.load params[:yaml]
1819
# ...
1920
end
21+
22+
def oj_example
23+
object = Oj.load params[:json]
24+
# ...
25+
end
2026
end

ql/src/queries/security/cwe-502/examples/UnsafeDeserializationGood.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,11 @@ def safe_yaml_example
1010
object = YAML.safe_load params[:yaml]
1111
# ...
1212
end
13+
14+
def safe_oj_example
15+
object = Oj.load params[:yaml], { mode: :strict }
16+
# or
17+
object = Oj.safe_load params[:yaml]
18+
# ...
19+
end
1320
end

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

Lines changed: 0 additions & 24 deletions
This file was deleted.

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

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require "oj"
2+
3+
class UsersController < ActionController::Base
4+
# GOOD - Oj.load is safe when any mode other than :object is set globally
5+
def route0
6+
json_data = params[:key]
7+
object = Oj.load json_data
8+
end
9+
10+
# BAD - the safe mode set globally is overridden with an unsafe mode passed as
11+
# a call argument
12+
def route1
13+
json_data = params[:key]
14+
object = Oj.load json_data, mode: :object
15+
end
16+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
require "oj"
2+
3+
# Set the default mode for the Oj library to use the :compat mode, which makes
4+
# Oj.load safe for untrusted data.
5+
Oj.default_options = { :mode => :compat }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
edges
2+
| OjGlobalOptions.rb:13:17:13:22 | call to params : | OjGlobalOptions.rb:14:22:14:30 | json_data |
3+
nodes
4+
| OjGlobalOptions.rb:13:17:13:22 | call to params : | semmle.label | call to params : |
5+
| OjGlobalOptions.rb:14:22:14:30 | json_data | semmle.label | json_data |
6+
subpaths
7+
#select
8+
| OjGlobalOptions.rb:14:22:14:30 | json_data | OjGlobalOptions.rb:13:17:13:22 | call to params : | OjGlobalOptions.rb:14:22:14:30 | json_data | Unsafe deserialization of $@. | OjGlobalOptions.rb:13:17:13:22 | call to params | user input |

0 commit comments

Comments
 (0)