@@ -126,42 +126,42 @@ module UnsafeDeserialization {
126126 }
127127 }
128128
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 ) {
136133 p .getKey ( ) .getConstantValue ( ) .isStringlikeValue ( "mode" ) and
137134 DataFlow:: exprNode ( p .getValue ( ) ) .getALocalSource ( ) .getConstantValue ( ) .isSymbol ( modeValue )
138135 }
139136
140137 /**
141138 * A node representing a hash that contains the key `:mode`.
142139 */
143- private class OjOptionsHashWithModeKey extends DataFlow:: Node {
140+ private class OptionsHashWithModeKey extends DataFlow:: Node {
144141 private string modeValue ;
145142
146- OjOptionsHashWithModeKey ( ) {
143+ OptionsHashWithModeKey ( ) {
147144 exists ( DataFlow:: LocalSourceNode options |
148145 options .flowsTo ( this ) and
149- isOjModePair ( options .asExpr ( ) .( CfgNodes:: ExprNodes:: HashLiteralCfgNode ) .getAKeyValuePair ( ) ,
146+ isModePair ( options .asExpr ( ) .( CfgNodes:: ExprNodes:: HashLiteralCfgNode ) .getAKeyValuePair ( ) ,
150147 modeValue )
151148 )
152149 }
153150
154151 /**
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`
157153 */
158- predicate hasKnownMode ( boolean isSafe ) { modeValue = getAKnownOjModeName ( isSafe ) }
154+ predicate hasKnownMode ( string mode ) { modeValue = mode }
155+ }
159156
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
165165 }
166166
167167 /**
@@ -197,9 +197,9 @@ module UnsafeDeserialization {
197197 */
198198 predicate hasExplicitKnownMode ( boolean isSafe ) {
199199 exists ( DataFlow:: Node arg , int i | i >= 1 and arg = this .getArgument ( i ) |
200- arg .( OjOptionsHashWithModeKey ) .hasKnownMode ( isSafe )
200+ arg .( OptionsHashWithModeKey ) .hasKnownMode ( getAKnownOjModeName ( isSafe ) )
201201 or
202- isOjModePair ( arg .asExpr ( ) , getAKnownOjModeName ( isSafe ) )
202+ isModePair ( arg .asExpr ( ) , getAKnownOjModeName ( isSafe ) )
203203 )
204204 }
205205 }
@@ -223,13 +223,109 @@ module UnsafeDeserialization {
223223 // anywhere to set the default options to a known safe mode.
224224 not ojLoad .hasExplicitKnownMode ( _) and
225225 not exists ( SetOjDefaultOptionsCall setOpts |
226- setOpts .getValue ( ) .( OjOptionsHashWithModeKey ) . hasSafeMode ( )
226+ setOpts .getValue ( ) .( OptionsHashWithModeKey ) . hasKnownMode ( getAKnownOjModeName ( true ) )
227227 )
228228 )
229229 )
230230 }
231231 }
232232
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+
233329 /**
234330 * An argument in a call to `Plist.parse_xml` where `marshal` is `true` (which is
235331 * the default), considered a sink for unsafe deserialization.
0 commit comments