1+ /**
2+ * Provides classes and predicates for detecting unsafe usage of
3+ * Newtonsoft.Json `TypeNameHandling` settings.
4+ *
5+ * Setting `TypeNameHandling` to any value other than `None` during
6+ * deserialization can enable remote code execution if untrusted data
7+ * is deserialized without a custom `SerializationBinder`.
8+ */
9+
110import csharp
211import semmle.code.csharp.security.dataflow.flowsources.Remote
312import semmle.code.csharp.serialization.Deserializers
413
14+ // ---------------------------------------------------------------------------
15+ // Source expressions: unsafe TypeNameHandling values
16+ // ---------------------------------------------------------------------------
17+ /**
18+ * An expression that represents an unsafe `TypeNameHandling` value —
19+ * any member of the `TypeNameHandling` enum other than `None`, or an
20+ * integer literal greater than zero (which maps to a non-`None` value).
21+ */
522class BadTypeHandling extends Expr {
623 BadTypeHandling ( ) {
724 exists ( Enum e , EnumConstant c |
825 e .hasFullyQualifiedName ( "Newtonsoft.Json" , "TypeNameHandling" ) and
926 c = e .getAnEnumConstant ( ) and
1027 this = c .getAnAccess ( ) and
1128 not c .hasName ( "None" )
12- ) or
29+ )
30+ or
1331 this .( IntegerLiteral ) .getValue ( ) .toInt ( ) > 0
1432 }
1533}
1634
35+ // ---------------------------------------------------------------------------
36+ // TypeNameHandling property modelling
37+ // ---------------------------------------------------------------------------
38+ /**
39+ * The `TypeNameHandling` property on `JsonSerializerSettings` or `JsonSerializer`.
40+ */
1741class TypeNameHandlingProperty extends Property {
18- TypeNameHandlingProperty ( ) {
19- this .hasFullyQualifiedName ( "Newtonsoft.Json" , [ "JsonSerializerSettings" , "JsonSerializer" ] , "TypeNameHandling" )
20- }
42+ TypeNameHandlingProperty ( ) {
43+ this .hasFullyQualifiedName ( "Newtonsoft.Json" ,
44+ [ "JsonSerializerSettings" , "JsonSerializer" ] , "TypeNameHandling" )
45+ }
2146}
2247
48+ /** A write to a `TypeNameHandling` property. */
2349class TypeNameHandlingPropertyWrite extends PropertyWrite {
2450 TypeNameHandlingPropertyWrite ( ) { this .getProperty ( ) instanceof TypeNameHandlingProperty }
2551
52+ /** Gets the right-hand side value assigned to this property. */
2653 Expr getAssignedValue ( ) {
2754 exists ( AssignExpr a |
2855 a .getLValue ( ) = this and
@@ -31,33 +58,80 @@ class TypeNameHandlingPropertyWrite extends PropertyWrite {
3158 }
3259}
3360
61+ /**
62+ * A write to a `TypeNameHandling` property where the assigned value is
63+ * known to be unsafe (i.e. not `None`).
64+ */
3465class BadTypeHandlingPropertyWrite extends TypeNameHandlingPropertyWrite {
3566 BadTypeHandlingPropertyWrite ( ) {
36- exists ( BadTypeHandling b |
37- DataFlow:: localExprFlow ( b , this .getAssignedValue ( ) )
67+ exists ( BadTypeHandling b |
68+ DataFlow:: localExprFlow ( b , this .getAssignedValue ( ) )
3869 )
3970 }
4071}
4172
73+ // ---------------------------------------------------------------------------
74+ // Binder property modelling
75+ // ---------------------------------------------------------------------------
76+ /**
77+ * A write to the `SerializationBinder` or `Binder` property on
78+ * `JsonSerializerSettings` or `JsonSerializer`. Setting a custom binder
79+ * is a mitigation against unsafe `TypeNameHandling`.
80+ */
4281class BinderPropertyWrite extends PropertyWrite {
4382 BinderPropertyWrite ( ) {
44- this .getProperty ( ) .hasFullyQualifiedName ( "Newtonsoft.Json" , [ "JsonSerializerSettings" , "JsonSerializer" ] , [ "SerializationBinder" , "Binder" ] )
83+ this .getProperty ( )
84+ .hasFullyQualifiedName ( "Newtonsoft.Json" ,
85+ [ "JsonSerializerSettings" , "JsonSerializer" ] ,
86+ [ "SerializationBinder" , "Binder" ] )
4587 }
4688}
4789
48- module UserInputToDeserializeObjectCallConfig implements DataFlow:: ConfigSig {
49- predicate isSource ( DataFlow:: Node source ) {
50- source instanceof RemoteFlowSource
90+ // ---------------------------------------------------------------------------
91+ // Deserialize call argument modelling
92+ // ---------------------------------------------------------------------------
93+ /**
94+ * An argument passed to a `Newtonsoft.Json.JsonConvert.DeserializeObject` call.
95+ */
96+ class DeserializeArg extends Expr {
97+ MethodCall deserializeCall ;
98+
99+ DeserializeArg ( ) {
100+ deserializeCall .getTarget ( ) instanceof NewtonsoftJsonConvertClassDeserializeObjectMethod and
101+ deserializeCall .getAnArgument ( ) = this
51102 }
52103
104+ /** Gets the enclosing `DeserializeObject` method call. */
105+ MethodCall getDeserializeCall ( ) { result = deserializeCall }
106+ }
107+
108+ /**
109+ * A `JsonSerializerSettings`-typed argument passed to a `DeserializeObject`
110+ * call. This is the primary sink for unsafe `TypeNameHandling` flow.
111+ */
112+ class JsonSerializerSettingsArg extends DeserializeArg {
113+ JsonSerializerSettingsArg ( ) { this .getType ( ) instanceof JsonSerializerSettingsClass }
114+ }
115+
116+ // ---------------------------------------------------------------------------
117+ // Taint tracking: remote input → DeserializeObject
118+ // ---------------------------------------------------------------------------
119+ /**
120+ * Tracks tainted data flowing from remote sources into arguments of
121+ * `JsonConvert.DeserializeObject`, including flow through `ToString()` calls.
122+ */
123+ module UserInputToDeserializeObjectCallConfig implements DataFlow:: ConfigSig {
124+ predicate isSource ( DataFlow:: Node source ) { source instanceof RemoteFlowSource }
125+
53126 predicate isSink ( DataFlow:: Node sink ) {
54- exists ( MethodCall mc |
55- mc .getTarget ( ) .hasFullyQualifiedName ( "Newtonsoft.Json.JsonConvert" , _) and
56- mc .getTarget ( ) .hasUndecoratedName ( "DeserializeObject" ) and
127+ exists ( MethodCall mc |
128+ mc .getTarget ( ) .hasFullyQualifiedName ( "Newtonsoft.Json.JsonConvert" , _) and
129+ mc .getTarget ( ) .hasUndecoratedName ( "DeserializeObject" ) and
57130 sink .asExpr ( ) = mc .getAnArgument ( )
58131 )
59132 }
60- predicate isAdditionalFlowStep ( DataFlow:: Node pred , DataFlow:: Node succ ) {
133+
134+ predicate isAdditionalFlowStep ( DataFlow:: Node pred , DataFlow:: Node succ ) {
61135 exists ( MethodCall ma |
62136 ma .getTarget ( ) .hasName ( "ToString" ) and
63137 ma .getQualifier ( ) = pred .asExpr ( ) and
@@ -66,94 +140,108 @@ module UserInputToDeserializeObjectCallConfig implements DataFlow::ConfigSig {
66140 }
67141}
68142
69- module UserInputToDeserializeObjectCallFlow = TaintTracking:: Global< UserInputToDeserializeObjectCallConfig > ;
143+ module UserInputToDeserializeObjectCallFlow =
144+ TaintTracking:: Global< UserInputToDeserializeObjectCallConfig > ;
70145
146+ // ---------------------------------------------------------------------------
147+ // Data flow: binder set on settings object
148+ // ---------------------------------------------------------------------------
149+ /**
150+ * Tracks whether a `SerializationBinder` is assigned via an object
151+ * initializer and the resulting settings object flows to a
152+ * `DeserializeObject` call argument.
153+ */
71154module BinderConfig implements DataFlow:: ConfigSig {
72- predicate isSource ( DataFlow:: Node source ) {
155+ predicate isSource ( DataFlow:: Node source ) {
73156 exists ( ObjectCreation oc , MemberInitializer mi |
74157 oc .getInitializer ( ) .( ObjectInitializer ) .getAMemberInitializer ( ) = mi and
75158 mi .getInitializedMember ( ) .hasName ( [ "Binder" , "SerializationBinder" ] ) and
76159 source .asExpr ( ) = oc
77160 )
78161 }
79- predicate isSink ( DataFlow:: Node sink ) {
80- sink .asExpr ( ) instanceof JsonSerializerSettingsArg
81- }
82- }
83- module BinderFlow = DataFlow:: Global< BinderConfig > ;
84162
85- class DeserializeArg extends Expr {
86- MethodCall deserializeCall ;
87- DeserializeArg ( ) {
88- deserializeCall .getTarget ( ) instanceof NewtonsoftJsonConvertClassDeserializeObjectMethod and
89- deserializeCall .getAnArgument ( ) = this
90- }
91- MethodCall getDeserializeCall ( ) {
92- result = deserializeCall
93- }
163+ predicate isSink ( DataFlow:: Node sink ) { sink .asExpr ( ) instanceof JsonSerializerSettingsArg }
94164}
95165
96- class JsonSerializerSettingsArg extends DeserializeArg {
97- JsonSerializerSettingsArg ( ) {
98- this .getType ( ) instanceof JsonSerializerSettingsClass
99- }
100- }
166+ module BinderFlow = DataFlow:: Global< BinderConfig > ;
101167
168+ // ---------------------------------------------------------------------------
169+ // Binder-set check (mitigation detection)
170+ // ---------------------------------------------------------------------------
171+ /**
172+ * Holds if a custom `SerializationBinder` or `Binder` has been set on the
173+ * settings object referenced by `arg`, either through an object initializer
174+ * (tracked via `BinderFlow`) or through a later property write on the same
175+ * variable.
176+ */
102177predicate hasBinderSet ( JsonSerializerSettingsArg arg ) {
103- // //passed as argument to initializer
104- exists ( BinderFlow:: PathNode sink |
178+ // Binder was set in an object initializer and flowed to `arg`
179+ exists ( BinderFlow:: PathNode sink |
105180 sink .isSink ( ) and
106181 sink .getNode ( ) .asExpr ( ) = arg
107- ) or
108- //set in later Propertywrite
182+ )
183+ or
184+ // Binder was set via a property write on the same variable
109185 exists ( PropertyWrite pw |
110186 pw .getProperty ( ) .hasName ( [ "Binder" , "SerializationBinder" ] ) and
111187 pw .getQualifier ( ) .( Access ) .getTarget ( ) = arg .( Access ) .getTarget ( )
112188 )
113189}
114190
191+ // ---------------------------------------------------------------------------
192+ // Sink node: TypeNameHandling property write value
193+ // ---------------------------------------------------------------------------
194+ /**
195+ * A data-flow node representing the value assigned to a `TypeNameHandling`
196+ * property. Provides a predicate to check whether a mitigating binder is set.
197+ */
115198class TypeNameHandlingPropertySink extends DataFlow:: Node {
116199 TypeNameHandlingPropertySink ( ) {
117200 exists ( TypeNameHandlingPropertyWrite pw |
118201 this .asExpr ( ) = pw .getAssignedValue ( )
119202 )
120203 }
121204
205+ /** Holds if a custom binder is set on the same settings object. */
122206 predicate hasBinderSet ( ) {
123207 exists ( JsonSerializerSettingsArg arg |
124208 this .asExpr ( ) = arg and
125209 hasBinderSet ( arg )
126210 )
127211 }
128-
129212}
130213
214+ // ---------------------------------------------------------------------------
215+ // Main flow: unsafe TypeNameHandling value → DeserializeObject settings arg
216+ // ---------------------------------------------------------------------------
217+ /**
218+ * Tracks unsafe `TypeNameHandling` values flowing into `JsonSerializerSettings`
219+ * arguments of `DeserializeObject` calls. Includes additional flow steps for
220+ * integer-to-enum casts and property writes on settings objects.
221+ */
131222module UnsafeTypeNameHandlingFlowConfig implements DataFlow:: ConfigSig {
132- predicate isSource ( DataFlow:: Node source ) {
133- source .asExpr ( ) instanceof BadTypeHandling
134- }
223+ predicate isSource ( DataFlow:: Node source ) { source .asExpr ( ) instanceof BadTypeHandling }
135224
136- predicate isSink ( DataFlow:: Node sink ) {
137- sink .asExpr ( ) instanceof JsonSerializerSettingsArg
138- }
225+ predicate isSink ( DataFlow:: Node sink ) { sink .asExpr ( ) instanceof JsonSerializerSettingsArg }
139226
140227 predicate isBarrierIn ( DataFlow:: Node node ) { isSource ( node ) }
141228
142229 predicate isAdditionalFlowStep ( DataFlow:: Node node1 , DataFlow:: Node node2 ) {
230+ // Cast from integer literal to TypeNameHandling enum
143231 node1 .asExpr ( ) instanceof IntegerLiteral and
144232 node2 .asExpr ( ) .( CastExpr ) .getExpr ( ) = node1 .asExpr ( )
145233 or
146234 node1 .getType ( ) instanceof TypeNameHandlingEnum and
147235 exists ( TypeNameHandlingPropertyWrite pw , Assignment a |
148236 a .getLValue ( ) = pw and
149237 (
150- // Explicit property write: flow from the assigned value to the JsonSerializerSettingsArg
151- // that accesses the same settings variable
238+ // Explicit property write: flow from the assigned value to the
239+ // JsonSerializerSettingsArg that accesses the same settings variable
152240 node1 .asExpr ( ) = a .getRValue ( ) and
153241 node2 .asExpr ( ) .( JsonSerializerSettingsArg ) .( VariableAccess ) .getTarget ( ) =
154242 pw .getQualifier ( ) .( VariableAccess ) .getTarget ( )
155243 or
156- // ObjectInitializer case : flow from the member initializer value to the
244+ // Object initializer : flow from the member initializer value to the
157245 // ObjectCreation, which then flows locally to the JsonSerializerSettingsArg
158246 exists ( ObjectInitializer oi , ObjectCreation oc |
159247 node1 .asExpr ( ) = oi .getAMemberInitializer ( ) .getRValue ( ) and
@@ -167,52 +255,65 @@ module UnsafeTypeNameHandlingFlowConfig implements DataFlow::ConfigSig {
167255
168256module UnsafeTypeNameHandlingFlow = DataFlow:: Global< UnsafeTypeNameHandlingFlowConfig > ;
169257
258+ // ---------------------------------------------------------------------------
259+ // Settings / serializer object creation modelling
260+ // ---------------------------------------------------------------------------
170261/**
171- * An ObjectCreation of type `Newtonson.Json.JsonSerializerSettings.JsonSerializerSettings` or `Newtonson.Json.JsonSerializerSettings.JsonSerializer`.
262+ * An `ObjectCreation` of type `Newtonsoft.Json.JsonSerializerSettings` or
263+ * `Newtonsoft.Json.JsonSerializer`.
172264 */
173265class JsonSerializerSettingsCreation extends ObjectCreation {
174266 JsonSerializerSettingsCreation ( ) {
175267 this .getTarget ( )
176- .hasFullyQualifiedName ( "Newtonsoft.Json.JsonSerializerSettings" , "JsonSerializerSettings" ) or
268+ .hasFullyQualifiedName ( "Newtonsoft.Json.JsonSerializerSettings" ,
269+ "JsonSerializerSettings" )
270+ or
177271 this .getTarget ( )
178- .hasFullyQualifiedName ( "Newtonsoft.Json.JsonSerializer" , "JsonSerializer" )
272+ .hasFullyQualifiedName ( "Newtonsoft.Json.JsonSerializer" , "JsonSerializer" )
179273 }
180- Class getAssignedBinderType ( ) {
181- exists ( AssignExpr ae |
274+
275+ /** Gets the type of the binder assigned to this settings object. */
276+ Class getAssignedBinderType ( ) {
277+ exists ( AssignExpr ae |
182278 ae .getLValue ( ) = this .getBinderPropertyWrite ( ) and
183- ae .getRValue ( ) .getType ( ) = result
279+ ae .getRValue ( ) .getType ( ) = result
184280 )
185281 }
186282
187- BinderPropertyWrite getBinderPropertyWrite ( ) {
188- result = this .getPropertyWrite ( )
189- }
283+ /** Gets a `BinderPropertyWrite` associated with this settings object. */
284+ BinderPropertyWrite getBinderPropertyWrite ( ) { result = this .getPropertyWrite ( ) }
190285
286+ /** Gets a `TypeNameHandlingPropertyWrite` associated with this settings object. */
191287 TypeNameHandlingPropertyWrite getTypeNameHandlingPropertyWrite ( ) {
192288 result = this .getPropertyWrite ( )
193289 }
194290
195- PropertyWrite getPropertyWrite ( ) {
291+ /**
292+ * Gets a `PropertyWrite` associated with this settings object, via an
293+ * initializer, direct local flow, or an assignment to the same property.
294+ */
295+ PropertyWrite getPropertyWrite ( ) {
196296 result = this .getInitializer ( ) .getAChild * ( )
197297 or
198298 // Direct local flow via some intermediary
199299 DataFlow:: localExprFlow ( this , result .getQualifier ( ) )
200300 or
201301 // Local flow via property writes
202- hasPropertyWrite ( result )
302+ this . hasPropertyWrite ( result )
203303 }
204304
205305 /**
206- * The qualifier to `pw` is a property, which this value flows into somewhere locally.
207- * Janky local DF.
208- * * /
306+ * Holds if `pw` is a property write on the same target that this object
307+ * creation is assigned to, within the same callable.
308+ */
209309 bindingset [ pw]
210310 pragma [ inline_late]
211- predicate hasPropertyWrite ( PropertyWrite pw ) {
311+ predicate hasPropertyWrite ( PropertyWrite pw ) {
212312 exists ( Assignment a |
213- a .getRValue ( ) = this
214- and a .getLValue ( ) .( PropertyAccess ) .getTarget ( ) = pw .getQualifier ( ) .( PropertyAccess ) .getTarget ( )
215- and a .getEnclosingCallable ( ) = pw .getEnclosingCallable ( )
313+ a .getRValue ( ) = this and
314+ a .getLValue ( ) .( PropertyAccess ) .getTarget ( ) =
315+ pw .getQualifier ( ) .( PropertyAccess ) .getTarget ( ) and
316+ a .getEnclosingCallable ( ) = pw .getEnclosingCallable ( )
216317 )
217318 }
218319}
0 commit comments