@@ -126,42 +126,42 @@ module UnsafeDeserialization {
126
126
}
127
127
}
128
128
129
- private string getAKnownOjModeName ( boolean isSafe ) {
130
- result = [ "compat" , "custom" , "json" , "null" , "rails" , "strict" , "wab" ] and isSafe = true
131
- or
132
- result = "object" and isSafe = false
133
- }
134
-
135
- private predicate isOjModePair ( CfgNodes:: ExprNodes:: PairCfgNode p , string modeValue ) {
129
+ /**
130
+ * Oj/Ox common code to establish whether a deserialization mode is defined.
131
+ */
132
+ private predicate isModePair ( CfgNodes:: ExprNodes:: PairCfgNode p , string modeValue ) {
136
133
p .getKey ( ) .getConstantValue ( ) .isStringlikeValue ( "mode" ) and
137
134
DataFlow:: exprNode ( p .getValue ( ) ) .getALocalSource ( ) .getConstantValue ( ) .isSymbol ( modeValue )
138
135
}
139
136
140
137
/**
141
138
* A node representing a hash that contains the key `:mode`.
142
139
*/
143
- private class OjOptionsHashWithModeKey extends DataFlow:: Node {
140
+ private class OptionsHashWithModeKey extends DataFlow:: Node {
144
141
private string modeValue ;
145
142
146
- OjOptionsHashWithModeKey ( ) {
143
+ OptionsHashWithModeKey ( ) {
147
144
exists ( DataFlow:: LocalSourceNode options |
148
145
options .flowsTo ( this ) and
149
- isOjModePair ( options .asExpr ( ) .( CfgNodes:: ExprNodes:: HashLiteralCfgNode ) .getAKeyValuePair ( ) ,
146
+ isModePair ( options .asExpr ( ) .( CfgNodes:: ExprNodes:: HashLiteralCfgNode ) .getAKeyValuePair ( ) ,
150
147
modeValue )
151
148
)
152
149
}
153
150
154
151
/**
155
- * Holds if this hash node contains a `:mode` key whose value is one known
156
- * to be `isSafe` with untrusted data.
152
+ * Holds if this hash node contains the `:mode`
157
153
*/
158
- predicate hasKnownMode ( boolean isSafe ) { modeValue = getAKnownOjModeName ( isSafe ) }
154
+ predicate hasKnownMode ( string mode ) { modeValue = mode }
155
+ }
159
156
160
- /**
161
- * Holds if this hash node contains a `:mode` key whose value is one of the
162
- * `Oj` modes known to be safe to use with untrusted data.
163
- */
164
- predicate hasSafeMode ( ) { this .hasKnownMode ( true ) }
157
+ /**
158
+ * Unsafe deserialization utilizing the Oj gem
159
+ * See: https://github.com/ohler55/oj
160
+ */
161
+ private string getAKnownOjModeName ( boolean isSafe ) {
162
+ result = [ "compat" , "custom" , "json" , "null" , "rails" , "strict" , "wab" ] and isSafe = true
163
+ or
164
+ result = "object" and isSafe = false
165
165
}
166
166
167
167
/**
@@ -179,10 +179,7 @@ module UnsafeDeserialization {
179
179
/**
180
180
* Gets the value being assigned to `Oj.default_options`.
181
181
*/
182
- DataFlow:: Node getValue ( ) {
183
- result .asExpr ( ) =
184
- this .getArgument ( 0 ) .asExpr ( ) .( CfgNodes:: ExprNodes:: AssignExprCfgNode ) .getRhs ( )
185
- }
182
+ DataFlow:: Node getValue ( ) { result = this .getArgument ( 0 ) }
186
183
}
187
184
188
185
/**
@@ -197,9 +194,9 @@ module UnsafeDeserialization {
197
194
*/
198
195
predicate hasExplicitKnownMode ( boolean isSafe ) {
199
196
exists ( DataFlow:: Node arg , int i | i >= 1 and arg = this .getArgument ( i ) |
200
- arg .( OjOptionsHashWithModeKey ) .hasKnownMode ( isSafe )
197
+ arg .( OptionsHashWithModeKey ) .hasKnownMode ( getAKnownOjModeName ( isSafe ) )
201
198
or
202
- isOjModePair ( arg .asExpr ( ) , getAKnownOjModeName ( isSafe ) )
199
+ isModePair ( arg .asExpr ( ) , getAKnownOjModeName ( isSafe ) )
203
200
)
204
201
}
205
202
}
@@ -223,13 +220,106 @@ module UnsafeDeserialization {
223
220
// anywhere to set the default options to a known safe mode.
224
221
not ojLoad .hasExplicitKnownMode ( _) and
225
222
not exists ( SetOjDefaultOptionsCall setOpts |
226
- setOpts .getValue ( ) .( OjOptionsHashWithModeKey ) .hasSafeMode ( )
223
+ setOpts .getValue ( ) .( OptionsHashWithModeKey ) .hasKnownMode ( getAKnownOjModeName ( true ) )
224
+ )
225
+ )
226
+ )
227
+ }
228
+ }
229
+
230
+ /**
231
+ * The first argument in a call to `Oj.object_load`, always considered as a
232
+ * sink for unsafe deserialization. (global and local mode options are ignored)
233
+ */
234
+ private class OjObjectLoadArgument extends Sink {
235
+ OjObjectLoadArgument ( ) {
236
+ this = API:: getTopLevelMember ( "Oj" ) .getAMethodCall ( "object_load" ) .getArgument ( 0 )
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Unsafe deserialization utilizing the Ox gem
242
+ * See: https://github.com/ohler55/ox
243
+ */
244
+ private string getAKnownOxModeName ( boolean isSafe ) {
245
+ result = [ "generic" , "limited" , "hash" , "hash_no_attrs" ] and isSafe = true
246
+ or
247
+ result = "object" and isSafe = false
248
+ }
249
+
250
+ /**
251
+ * A call node that sets `Ox.default_options`.
252
+ *
253
+ * ```rb
254
+ * Ox.default_options = { mode: :limited, effort: :tolerant }
255
+ * ```
256
+ */
257
+ private class SetOxDefaultOptionsCall extends DataFlow:: CallNode {
258
+ SetOxDefaultOptionsCall ( ) {
259
+ this = API:: getTopLevelMember ( "Ox" ) .getAMethodCall ( "default_options=" )
260
+ }
261
+
262
+ /**
263
+ * Gets the value being assigned to `Ox.default_options`.
264
+ */
265
+ DataFlow:: Node getValue ( ) { result = this .getArgument ( 0 ) }
266
+ }
267
+
268
+ /**
269
+ * A call to `Ox.load`.
270
+ */
271
+ private class OxLoadCall extends DataFlow:: CallNode {
272
+ OxLoadCall ( ) { this = API:: getTopLevelMember ( "Ox" ) .getAMethodCall ( "load" ) }
273
+
274
+ /**
275
+ * Holds if this call to `Ox.load` includes an explicit options hash
276
+ * argument that sets the mode to one that is known to be `isSafe`.
277
+ */
278
+ predicate hasExplicitKnownMode ( boolean isSafe ) {
279
+ exists ( DataFlow:: Node arg , int i | i >= 1 and arg = this .getArgument ( i ) |
280
+ arg .( OptionsHashWithModeKey ) .hasKnownMode ( getAKnownOxModeName ( isSafe ) )
281
+ or
282
+ isModePair ( arg .asExpr ( ) , getAKnownOxModeName ( isSafe ) )
283
+ )
284
+ }
285
+ }
286
+
287
+ /**
288
+ * An argument in a call to `Ox.load` where the mode is `:object` (not the default),
289
+ * considered a sink for unsafe deserialization.
290
+ */
291
+ class UnsafeOxLoadArgument extends Sink {
292
+ UnsafeOxLoadArgument ( ) {
293
+ exists ( OxLoadCall oxLoad |
294
+ this = oxLoad .getArgument ( 0 ) and
295
+ // Exclude calls that explicitly pass a safe mode option.
296
+ not oxLoad .hasExplicitKnownMode ( true ) and
297
+ (
298
+ // Sinks to include:
299
+ // - Calls with an explicit, unsafe mode option.
300
+ oxLoad .hasExplicitKnownMode ( false )
301
+ or
302
+ // - Calls with no explicit mode option and there exists a call
303
+ // anywhere to set the default options to an unsafe mode (object).
304
+ not oxLoad .hasExplicitKnownMode ( _) and
305
+ exists ( SetOxDefaultOptionsCall setOpts |
306
+ setOpts .getValue ( ) .( OptionsHashWithModeKey ) .hasKnownMode ( getAKnownOxModeName ( false ) )
227
307
)
228
308
)
229
309
)
230
310
}
231
311
}
232
312
313
+ /**
314
+ * The first argument in a call to `Ox.parse_obj`, always considered as a
315
+ * sink for unsafe deserialization.
316
+ */
317
+ class OxParseObjArgument extends Sink {
318
+ OxParseObjArgument ( ) {
319
+ this = API:: getTopLevelMember ( "Ox" ) .getAMethodCall ( "parse_obj" ) .getArgument ( 0 )
320
+ }
321
+ }
322
+
233
323
/**
234
324
* An argument in a call to `Plist.parse_xml` where `marshal` is `true` (which is
235
325
* the default), considered a sink for unsafe deserialization.
0 commit comments