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+
10+ import csharp
11+ import semmle.code.csharp.security.dataflow.flowsources.Remote
12+ import semmle.code.csharp.serialization.Deserializers
13+
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+ */
22+ class BadTypeHandling extends Expr {
23+ BadTypeHandling ( ) {
24+ exists ( Enum e , EnumConstant c |
25+ e .hasFullyQualifiedName ( "Newtonsoft.Json" , "TypeNameHandling" ) and
26+ c = e .getAnEnumConstant ( ) and
27+ this = c .getAnAccess ( ) and
28+ not c .hasName ( "None" )
29+ )
30+ or
31+ this .( IntegerLiteral ) .getValue ( ) .toInt ( ) > 0
32+ }
33+ }
34+
35+ // ---------------------------------------------------------------------------
36+ // TypeNameHandling property modelling
37+ // ---------------------------------------------------------------------------
38+ /**
39+ * The `TypeNameHandling` property on `JsonSerializerSettings` or `JsonSerializer`.
40+ */
41+ class TypeNameHandlingProperty extends Property {
42+ TypeNameHandlingProperty ( ) {
43+ this .hasFullyQualifiedName ( "Newtonsoft.Json" ,
44+ [ "JsonSerializerSettings" , "JsonSerializer" ] , "TypeNameHandling" )
45+ }
46+ }
47+
48+ /** A write to a `TypeNameHandling` property. */
49+ class TypeNameHandlingPropertyWrite extends PropertyWrite {
50+ TypeNameHandlingPropertyWrite ( ) { this .getProperty ( ) instanceof TypeNameHandlingProperty }
51+
52+ /** Gets the right-hand side value assigned to this property. */
53+ Expr getAssignedValue ( ) {
54+ exists ( AssignExpr a |
55+ a .getLValue ( ) = this and
56+ result = a .getRValue ( )
57+ )
58+ }
59+ }
60+
61+ /**
62+ * A write to a `TypeNameHandling` property where the assigned value is
63+ * known to be unsafe (i.e. not `None`).
64+ */
65+ class BadTypeHandlingPropertyWrite extends TypeNameHandlingPropertyWrite {
66+ BadTypeHandlingPropertyWrite ( ) {
67+ exists ( BadTypeHandling b |
68+ DataFlow:: localExprFlow ( b , this .getAssignedValue ( ) )
69+ )
70+ }
71+ }
72+
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+ */
81+ class BinderPropertyWrite extends PropertyWrite {
82+ BinderPropertyWrite ( ) {
83+ this .getProperty ( )
84+ .hasFullyQualifiedName ( "Newtonsoft.Json" ,
85+ [ "JsonSerializerSettings" , "JsonSerializer" ] ,
86+ [ "SerializationBinder" , "Binder" ] )
87+ }
88+ }
89+
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
102+ }
103+
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+
126+ predicate isSink ( DataFlow:: Node sink ) {
127+ exists ( MethodCall mc |
128+ mc .getTarget ( ) .hasFullyQualifiedName ( "Newtonsoft.Json.JsonConvert" , _) and
129+ mc .getTarget ( ) .hasUndecoratedName ( "DeserializeObject" ) and
130+ sink .asExpr ( ) = mc .getAnArgument ( )
131+ )
132+ }
133+
134+ predicate isAdditionalFlowStep ( DataFlow:: Node pred , DataFlow:: Node succ ) {
135+ exists ( MethodCall ma |
136+ ma .getTarget ( ) .hasName ( "ToString" ) and
137+ ma .getQualifier ( ) = pred .asExpr ( ) and
138+ succ .asExpr ( ) = ma
139+ )
140+ }
141+ }
142+
143+ module UserInputToDeserializeObjectCallFlow =
144+ TaintTracking:: Global< UserInputToDeserializeObjectCallConfig > ;
145+
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+ */
154+ module BinderConfig implements DataFlow:: ConfigSig {
155+ predicate isSource ( DataFlow:: Node source ) {
156+ exists ( ObjectCreation oc , MemberInitializer mi |
157+ oc .getInitializer ( ) .( ObjectInitializer ) .getAMemberInitializer ( ) = mi and
158+ mi .getInitializedMember ( ) .hasName ( [ "Binder" , "SerializationBinder" ] ) and
159+ source .asExpr ( ) = oc
160+ )
161+ }
162+
163+ predicate isSink ( DataFlow:: Node sink ) { sink .asExpr ( ) instanceof JsonSerializerSettingsArg }
164+ }
165+
166+ module BinderFlow = DataFlow:: Global< BinderConfig > ;
167+
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+ */
177+ predicate hasBinderSet ( JsonSerializerSettingsArg arg ) {
178+ // Binder was set in an object initializer and flowed to `arg`
179+ exists ( BinderFlow:: PathNode sink |
180+ sink .isSink ( ) and
181+ sink .getNode ( ) .asExpr ( ) = arg
182+ )
183+ or
184+ // Binder was set via a property write on the same variable
185+ exists ( PropertyWrite pw |
186+ pw .getProperty ( ) .hasName ( [ "Binder" , "SerializationBinder" ] ) and
187+ pw .getQualifier ( ) .( Access ) .getTarget ( ) = arg .( Access ) .getTarget ( )
188+ )
189+ }
190+
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+ */
198+ class TypeNameHandlingPropertySink extends DataFlow:: Node {
199+ TypeNameHandlingPropertySink ( ) {
200+ exists ( TypeNameHandlingPropertyWrite pw |
201+ this .asExpr ( ) = pw .getAssignedValue ( )
202+ )
203+ }
204+
205+ /** Holds if a custom binder is set on the same settings object. */
206+ predicate hasBinderSet ( ) {
207+ exists ( JsonSerializerSettingsArg arg |
208+ this .asExpr ( ) = arg and
209+ hasBinderSet ( arg )
210+ )
211+ }
212+ }
213+
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+ */
222+ module UnsafeTypeNameHandlingFlowConfig implements DataFlow:: ConfigSig {
223+ predicate isSource ( DataFlow:: Node source ) { source .asExpr ( ) instanceof BadTypeHandling }
224+
225+ predicate isSink ( DataFlow:: Node sink ) { sink .asExpr ( ) instanceof JsonSerializerSettingsArg }
226+
227+ predicate isBarrierIn ( DataFlow:: Node node ) { isSource ( node ) }
228+
229+ predicate isAdditionalFlowStep ( DataFlow:: Node node1 , DataFlow:: Node node2 ) {
230+ // Cast from integer literal to TypeNameHandling enum
231+ node1 .asExpr ( ) instanceof IntegerLiteral and
232+ node2 .asExpr ( ) .( CastExpr ) .getExpr ( ) = node1 .asExpr ( )
233+ or
234+ node1 .getType ( ) instanceof TypeNameHandlingEnum and
235+ exists ( TypeNameHandlingPropertyWrite pw , Assignment a |
236+ a .getLValue ( ) = pw and
237+ (
238+ // Explicit property write: flow from the assigned value to the
239+ // JsonSerializerSettingsArg that accesses the same settings variable
240+ node1 .asExpr ( ) = a .getRValue ( ) and
241+ node2 .asExpr ( ) .( JsonSerializerSettingsArg ) .( VariableAccess ) .getTarget ( ) =
242+ pw .getQualifier ( ) .( VariableAccess ) .getTarget ( )
243+ or
244+ // Object initializer: flow from the member initializer value to the
245+ // ObjectCreation, which then flows locally to the JsonSerializerSettingsArg
246+ exists ( ObjectInitializer oi , ObjectCreation oc |
247+ node1 .asExpr ( ) = oi .getAMemberInitializer ( ) .getRValue ( ) and
248+ oc .getInitializer ( ) = oi and
249+ DataFlow:: localExprFlow ( oc , node2 .asExpr ( ) .( JsonSerializerSettingsArg ) )
250+ )
251+ )
252+ )
253+ }
254+ }
255+
256+ module UnsafeTypeNameHandlingFlow = DataFlow:: Global< UnsafeTypeNameHandlingFlowConfig > ;
257+
258+ // ---------------------------------------------------------------------------
259+ // Settings / serializer object creation modelling
260+ // ---------------------------------------------------------------------------
261+ /**
262+ * An `ObjectCreation` of type `Newtonsoft.Json.JsonSerializerSettings` or
263+ * `Newtonsoft.Json.JsonSerializer`.
264+ */
265+ class JsonSerializerSettingsCreation extends ObjectCreation {
266+ JsonSerializerSettingsCreation ( ) {
267+ this .getTarget ( )
268+ .hasFullyQualifiedName ( "Newtonsoft.Json.JsonSerializerSettings" ,
269+ "JsonSerializerSettings" )
270+ or
271+ this .getTarget ( )
272+ .hasFullyQualifiedName ( "Newtonsoft.Json.JsonSerializer" , "JsonSerializer" )
273+ }
274+
275+ /** Gets the type of the binder assigned to this settings object. */
276+ Class getAssignedBinderType ( ) {
277+ exists ( AssignExpr ae |
278+ ae .getLValue ( ) = this .getBinderPropertyWrite ( ) and
279+ ae .getRValue ( ) .getType ( ) = result
280+ )
281+ }
282+
283+ /** Gets a `BinderPropertyWrite` associated with this settings object. */
284+ BinderPropertyWrite getBinderPropertyWrite ( ) { result = this .getPropertyWrite ( ) }
285+
286+ /** Gets a `TypeNameHandlingPropertyWrite` associated with this settings object. */
287+ TypeNameHandlingPropertyWrite getTypeNameHandlingPropertyWrite ( ) {
288+ result = this .getPropertyWrite ( )
289+ }
290+
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 ( ) {
296+ result = this .getInitializer ( ) .getAChild * ( )
297+ or
298+ // Direct local flow via some intermediary
299+ DataFlow:: localExprFlow ( this , result .getQualifier ( ) )
300+ or
301+ // Local flow via property writes
302+ this .hasPropertyWrite ( result )
303+ }
304+
305+ /**
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+ */
309+ bindingset [ pw]
310+ pragma [ inline_late]
311+ predicate hasPropertyWrite ( PropertyWrite pw ) {
312+ exists ( Assignment a |
313+ a .getRValue ( ) = this and
314+ a .getLValue ( ) .( PropertyAccess ) .getTarget ( ) =
315+ pw .getQualifier ( ) .( PropertyAccess ) .getTarget ( ) and
316+ a .getEnclosingCallable ( ) = pw .getEnclosingCallable ( )
317+ )
318+ }
319+ }
0 commit comments