@@ -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 /**
@@ -179,10 +179,7 @@ module UnsafeDeserialization {
179179 /**
180180 * Gets the value being assigned to `Oj.default_options`.
181181 */
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 ) }
186183 }
187184
188185 /**
@@ -197,9 +194,9 @@ module UnsafeDeserialization {
197194 */
198195 predicate hasExplicitKnownMode ( boolean isSafe ) {
199196 exists ( DataFlow:: Node arg , int i | i >= 1 and arg = this .getArgument ( i ) |
200- arg .( OjOptionsHashWithModeKey ) .hasKnownMode ( isSafe )
197+ arg .( OptionsHashWithModeKey ) .hasKnownMode ( getAKnownOjModeName ( isSafe ) )
201198 or
202- isOjModePair ( arg .asExpr ( ) , getAKnownOjModeName ( isSafe ) )
199+ isModePair ( arg .asExpr ( ) , getAKnownOjModeName ( isSafe ) )
203200 )
204201 }
205202 }
@@ -223,13 +220,106 @@ module UnsafeDeserialization {
223220 // anywhere to set the default options to a known safe mode.
224221 not ojLoad .hasExplicitKnownMode ( _) and
225222 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 ) )
227307 )
228308 )
229309 )
230310 }
231311 }
232312
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+
233323 /**
234324 * An argument in a call to `Plist.parse_xml` where `marshal` is `true` (which is
235325 * the default), considered a sink for unsafe deserialization.
0 commit comments