@@ -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
/**
@@ -197,9 +197,9 @@ module UnsafeDeserialization {
197
197
*/
198
198
predicate hasExplicitKnownMode ( boolean isSafe ) {
199
199
exists ( DataFlow:: Node arg , int i | i >= 1 and arg = this .getArgument ( i ) |
200
- arg .( OjOptionsHashWithModeKey ) .hasKnownMode ( isSafe )
200
+ arg .( OptionsHashWithModeKey ) .hasKnownMode ( getAKnownOjModeName ( isSafe ) )
201
201
or
202
- isOjModePair ( arg .asExpr ( ) , getAKnownOjModeName ( isSafe ) )
202
+ isModePair ( arg .asExpr ( ) , getAKnownOjModeName ( isSafe ) )
203
203
)
204
204
}
205
205
}
@@ -223,13 +223,109 @@ module UnsafeDeserialization {
223
223
// anywhere to set the default options to a known safe mode.
224
224
not ojLoad .hasExplicitKnownMode ( _) and
225
225
not exists ( SetOjDefaultOptionsCall setOpts |
226
- setOpts .getValue ( ) .( OjOptionsHashWithModeKey ) . hasSafeMode ( )
226
+ setOpts .getValue ( ) .( OptionsHashWithModeKey ) . hasKnownMode ( getAKnownOjModeName ( true ) )
227
227
)
228
228
)
229
229
)
230
230
}
231
231
}
232
232
233
+ /**
234
+ * The first argument in a call to `Oj.object_load`, always considered as a
235
+ * sink for unsafe deserialization. (global and local mode options are ignored)
236
+ */
237
+ class OjObjectLoadArgument extends Sink {
238
+ OjObjectLoadArgument ( ) {
239
+ this = API:: getTopLevelMember ( "Oj" ) .getAMethodCall ( "object_load" ) .getArgument ( 0 )
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Unsafe deserialization utilizing the Ox gem
245
+ * See: https://github.com/ohler55/ox
246
+ */
247
+ private string getAKnownOxModeName ( boolean isSafe ) {
248
+ result = [ "generic" , "limited" , "hash" , "hash_no_attrs" ] and isSafe = true
249
+ or
250
+ result = "object" and isSafe = false
251
+ }
252
+
253
+ /**
254
+ * A call node that sets `Ox.default_options`.
255
+ *
256
+ * ```rb
257
+ * Ox.default_options = { mode: :limited, effort: :tolerant }
258
+ * ```
259
+ */
260
+ private class SetOxDefaultOptionsCall extends DataFlow:: CallNode {
261
+ SetOxDefaultOptionsCall ( ) {
262
+ this = API:: getTopLevelMember ( "Ox" ) .getAMethodCall ( "default_options=" )
263
+ }
264
+
265
+ /**
266
+ * Gets the value being assigned to `Ox.default_options`.
267
+ */
268
+ DataFlow:: Node getValue ( ) {
269
+ result .asExpr ( ) =
270
+ this .getArgument ( 0 ) .asExpr ( ) .( CfgNodes:: ExprNodes:: AssignExprCfgNode ) .getRhs ( )
271
+ }
272
+ }
273
+
274
+ /**
275
+ * A call to `Ox.load`.
276
+ */
277
+ private class OxLoadCall extends DataFlow:: CallNode {
278
+ OxLoadCall ( ) { this = API:: getTopLevelMember ( "Ox" ) .getAMethodCall ( "load" ) }
279
+
280
+ /**
281
+ * Holds if this call to `Ox.load` includes an explicit options hash
282
+ * argument that sets the mode to one that is known to be `isSafe`.
283
+ */
284
+ predicate hasExplicitKnownMode ( boolean isSafe ) {
285
+ exists ( DataFlow:: Node arg , int i | i >= 1 and arg = this .getArgument ( i ) |
286
+ arg .( OptionsHashWithModeKey ) .hasKnownMode ( getAKnownOxModeName ( isSafe ) )
287
+ or
288
+ isModePair ( arg .asExpr ( ) , getAKnownOxModeName ( isSafe ) )
289
+ )
290
+ }
291
+ }
292
+
293
+ /**
294
+ * An argument in a call to `Ox.load` where the mode is `:object` (not the default),
295
+ * considered a sink for unsafe deserialization.
296
+ */
297
+ class UnsafeOxLoadArgument extends Sink {
298
+ UnsafeOxLoadArgument ( ) {
299
+ exists ( OxLoadCall oxLoad |
300
+ this = oxLoad .getArgument ( 0 ) and
301
+ // Exclude calls that explicitly pass a safe mode option.
302
+ not oxLoad .hasExplicitKnownMode ( true ) and
303
+ (
304
+ // Sinks to include:
305
+ // - Calls with an explicit, unsafe mode option.
306
+ oxLoad .hasExplicitKnownMode ( false )
307
+ or
308
+ // - Calls with no explicit mode option and there exists a call
309
+ // anywhere to set the default options to an unsafe mode (object).
310
+ not oxLoad .hasExplicitKnownMode ( _) and
311
+ exists ( SetOxDefaultOptionsCall setOpts |
312
+ setOpts .getValue ( ) .( OptionsHashWithModeKey ) .hasKnownMode ( getAKnownOxModeName ( false ) )
313
+ )
314
+ )
315
+ )
316
+ }
317
+ }
318
+
319
+ /**
320
+ * The first argument in a call to `Ox.parse_obj`, always considered as a
321
+ * sink for unsafe deserialization.
322
+ */
323
+ class OxParseObjArgument extends Sink {
324
+ OxParseObjArgument ( ) {
325
+ this = API:: getTopLevelMember ( "Ox" ) .getAMethodCall ( "parse_obj" ) .getArgument ( 0 )
326
+ }
327
+ }
328
+
233
329
/**
234
330
* An argument in a call to `Plist.parse_xml` where `marshal` is `true` (which is
235
331
* the default), considered a sink for unsafe deserialization.
0 commit comments