Skip to content

Commit 14bdea1

Browse files
authored
Merge pull request #847 from calumgrant/cs/json.net
C#: Model Json.NET dataflow
2 parents be3191a + c9cf183 commit 14bdea1

File tree

7 files changed

+416
-0
lines changed

7 files changed

+416
-0
lines changed

change-notes/1.20/analysis-csharp.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@
2727
## Changes to QL libraries
2828

2929
* The class `TrivialProperty` now includes library properties determined to be trivial using CIL analysis. This may increase the number of results for all queries that use data flow.
30+
* Taint-tracking steps have been added for the `Json.NET` package. This will improve results for queries that use taint-tracking.
3031

3132
## Changes to the autobuilder

csharp/ql/src/semmle/code/csharp/dataflow/TaintTracking.qll

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module TaintTracking {
99
private import semmle.code.csharp.dataflow.LibraryTypeDataFlow
1010
private import semmle.code.csharp.dispatch.Dispatch
1111
private import semmle.code.csharp.commons.ComparisonTest
12+
private import semmle.code.csharp.frameworks.JsonNET
1213
private import cil
1314
private import dotnet
1415

@@ -18,6 +19,9 @@ module TaintTracking {
1819
*/
1920
predicate localTaint(DataFlow::Node source, DataFlow::Node sink) { localTaintStep*(source, sink) }
2021

22+
/** A member (property or field) that is tainted if its containing object is tainted. */
23+
abstract class TaintedMember extends AssignableMember { }
24+
2125
/**
2226
* Holds if taint propagates from `nodeFrom` to `nodeTo` in exactly one local
2327
* (intra-procedural) step.
@@ -243,6 +247,16 @@ module TaintTracking {
243247
DataFlow::Internal::flowOutOfDelegateLibraryCall(nodeFrom, nodeTo, false)
244248
or
245249
localTaintStepCil(nodeFrom, nodeTo)
250+
or
251+
// Taint members
252+
exists(Access access |
253+
access = nodeTo.asExpr() and
254+
access.getTarget() instanceof TaintedMember
255+
|
256+
access.(FieldRead).getQualifier() = nodeFrom.asExpr()
257+
or
258+
access.(PropertyRead).getQualifier() = nodeFrom.asExpr()
259+
)
246260
}
247261
}
248262
import Cached
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/**
2+
* Classes for modelling Json.NET.
3+
*/
4+
5+
import csharp
6+
import semmle.code.csharp.dataflow.LibraryTypeDataFlow
7+
8+
/** Definitions relating to the `Json.NET` package. */
9+
module JsonNET {
10+
/** The namespace `Newtonsoft.Json`. */
11+
class JsonNETNamespace extends Namespace {
12+
JsonNETNamespace() { this.hasQualifiedName("Newtonsoft.Json") }
13+
}
14+
15+
/** A class in `Newtonsoft.Json`. */
16+
class JsonClass extends Class { JsonClass() { this.getParent() instanceof JsonNETNamespace } }
17+
18+
/** The class `Newtonsoft.Json.JsonConvert`. */
19+
class JsonConvertClass extends JsonClass, LibraryTypeDataFlow {
20+
JsonConvertClass() { this.hasName("JsonConvert") }
21+
22+
/** Gets a `ToString` method. */
23+
private Method getAToStringMethod() {
24+
result = this.getAMethod("ToString") and
25+
result.isStatic()
26+
}
27+
28+
/** Gets a `Deserialize` method. */
29+
Method getADeserializeMethod() {
30+
result = this.getAMethod() and
31+
result.getName().matches("Deserialize%")
32+
}
33+
34+
/** Gets a `Serialize` method. */
35+
Method getASerializeMethod() {
36+
result = this.getAMethod() and
37+
result.getName().matches("Serialize%")
38+
}
39+
40+
private Method getAPopulateMethod() {
41+
result = this.getAMethod() and
42+
result.getName().matches("Populate%")
43+
}
44+
45+
override predicate callableFlow(
46+
CallableFlowSource source, CallableFlowSink sink, SourceDeclarationCallable c,
47+
boolean preservesValue
48+
) {
49+
// ToString methods
50+
c = getAToStringMethod() and
51+
preservesValue = true and
52+
source = any(CallableFlowSourceArg arg | arg.getArgumentIndex() = 0) and
53+
sink instanceof CallableFlowSinkReturn
54+
or
55+
// Deserialize methods
56+
c = getADeserializeMethod() and
57+
preservesValue = false and
58+
source = any(CallableFlowSourceArg arg | arg.getArgumentIndex() = 0) and
59+
sink instanceof CallableFlowSinkReturn
60+
or
61+
// Serialize methods
62+
c = getASerializeMethod() and
63+
preservesValue = false and
64+
source = any(CallableFlowSourceArg arg | arg.getArgumentIndex() = 0) and
65+
sink instanceof CallableFlowSinkReturn
66+
or
67+
// Populate methods
68+
c = getAPopulateMethod() and
69+
preservesValue = false and
70+
source = any(CallableFlowSourceArg arg | arg.getArgumentIndex() = 0) and
71+
sink = any(CallableFlowSinkArg arg | arg.getArgumentIndex() = 1)
72+
}
73+
}
74+
75+
/** A type that is serialized. */
76+
private class SerializedType extends ValueOrRefType {
77+
SerializedType() {
78+
// Supplied as a parameter to a serialize or deserialize method
79+
exists(TypeParameter tp, JsonConvertClass jc, UnboundGenericMethod serializeMethod |
80+
this = tp.getAnUltimatelySuppliedType() and
81+
tp = serializeMethod.getATypeParameter()
82+
|
83+
serializeMethod = jc.getASerializeMethod()
84+
or
85+
serializeMethod = jc.getADeserializeMethod()
86+
)
87+
or
88+
exists(Class attribute | attribute = this.getAnAttribute().getType() |
89+
attribute instanceof JsonObjectAttributeClass
90+
or
91+
attribute.hasName("JsonConverterAttribute")
92+
)
93+
or
94+
this.getAConstructor().getAnAttribute().getType().hasName("JsonConstructorAttribute")
95+
}
96+
97+
predicate isOptIn() { this.getAnAttribute().(JsonObjectAttribute).isOptIn() }
98+
}
99+
100+
/**
101+
* A field/property that can be serialized, either explicitly
102+
* or as a member of a serialized type.
103+
*/
104+
private class SerializedMember extends TaintTracking::TaintedMember {
105+
SerializedMember() {
106+
// This member has a Json attribute
107+
exists(Class attribute | attribute = this.(Attributable).getAnAttribute().getType() |
108+
attribute.hasName("JsonPropertyAttribute")
109+
or
110+
attribute.hasName("JsonDictionaryAttribute")
111+
or
112+
attribute.hasName("JsonRequiredAttribute")
113+
or
114+
attribute.hasName("JsonArrayAttribute")
115+
or
116+
attribute.hasName("JsonConverterAttribute")
117+
or
118+
attribute.hasName("JsonExtensionDataAttribute")
119+
or
120+
attribute.hasName("SerializableAttribute") // System.SerializableAttribute
121+
or
122+
attribute.hasName("DataMemberAttribute") // System.DataMemberAttribute
123+
)
124+
or
125+
// This field is a member of an explicitly serialized type
126+
this.getDeclaringType() instanceof SerializedType and
127+
not this.getDeclaringType().(SerializedType).isOptIn() and
128+
not this.(Attributable).getAnAttribute().getType() instanceof NotSerializedAttributeClass
129+
}
130+
}
131+
132+
/** The class `NewtonSoft.Json.JsonSerializer`. */
133+
class JsonSerializerClass extends JsonClass, LibraryTypeDataFlow {
134+
JsonSerializerClass() { this.hasName("JsonSerializer") }
135+
136+
Method getSerializeMethod() { result = this.getAMethod("Serialize") }
137+
138+
Method getDeserializeMethod() { result = this.getAMethod("Deserialize") }
139+
140+
override predicate callableFlow(
141+
CallableFlowSource source, CallableFlowSink sink, SourceDeclarationCallable c,
142+
boolean preservesValue
143+
) {
144+
// Serialize
145+
c = this.getSerializeMethod() and
146+
preservesValue = false and
147+
source = any(CallableFlowSourceArg arg | arg.getArgumentIndex() = 0) and
148+
sink = any(CallableFlowSinkArg arg | arg.getArgumentIndex() = 1)
149+
or
150+
// Deserialize
151+
c = this.getDeserializeMethod() and
152+
preservesValue = false and
153+
source = any(CallableFlowSourceArg arg | arg.getArgumentIndex() = 0) and
154+
sink = any(CallableFlowSinkArg arg | arg.getArgumentIndex() = 1)
155+
}
156+
}
157+
158+
/** Any attribute class that marks a member to not be serialized. */
159+
private class NotSerializedAttributeClass extends JsonClass {
160+
NotSerializedAttributeClass() {
161+
this.hasName("JsonIgnoreAttribute") or this.hasName("NonSerializedAttribute")
162+
}
163+
}
164+
165+
/** The class `Newtonsoft.Json.ObjectAttribute`. */
166+
class JsonObjectAttributeClass extends JsonClass {
167+
JsonObjectAttributeClass() { this.hasName("JsonObjectAttribute") }
168+
}
169+
170+
/** An attribute of type `Newtonsoft.Json.ObjectAttribute`. */
171+
class JsonObjectAttribute extends Attribute {
172+
JsonObjectAttribute() { this.getType() instanceof JsonObjectAttributeClass }
173+
174+
/** Holds if the `OptIn` argument has been supplied to this attribute. */
175+
predicate isOptIn() { this.getArgument(_).(FieldAccess).getTarget().hasName("OptIn") }
176+
}
177+
178+
/** The namespace `Newtonsoft.Json.Linq`. */
179+
class LinqNamespace extends Namespace {
180+
LinqNamespace() {
181+
this.getParentNamespace() instanceof JsonNETNamespace and this.hasName("Linq")
182+
}
183+
}
184+
185+
/** A class in `Newtonsoft.Json.Linq`. */
186+
class LinqClass extends Class {
187+
LinqClass() { this.getDeclaringNamespace() instanceof LinqNamespace }
188+
}
189+
190+
/** The `NewtonSoft.Json.Linq.JObject` class. */
191+
class JObjectClass extends LinqClass, LibraryTypeDataFlow {
192+
JObjectClass() { this.hasName("JObject") }
193+
194+
override predicate callableFlow(
195+
CallableFlowSource source, CallableFlowSink sink, SourceDeclarationCallable c,
196+
boolean preservesValue
197+
) {
198+
// ToString method
199+
c = this.getAMethod("ToString") and
200+
source instanceof CallableFlowSourceQualifier and
201+
sink instanceof CallableFlowSinkReturn and
202+
preservesValue = false
203+
or
204+
// Parse method
205+
c = this.getParseMethod() and
206+
source = any(CallableFlowSourceArg arg | arg.getArgumentIndex() = 0) and
207+
sink instanceof CallableFlowSinkReturn and
208+
preservesValue = false
209+
or
210+
// operator string
211+
c = any(Operator op |
212+
op.getDeclaringType() = this.getABaseType*() and op.getReturnType() instanceof StringType
213+
) and
214+
source = any(CallableFlowSourceArg arg | arg.getArgumentIndex() = 0) and
215+
sink instanceof CallableFlowSinkReturn and
216+
preservesValue = false
217+
or
218+
// SelectToken method
219+
c = this.getSelectTokenMethod() and
220+
source instanceof CallableFlowSourceQualifier and
221+
sink instanceof CallableFlowSinkReturn and
222+
preservesValue = false
223+
}
224+
225+
/** Gets the `Parse` method. */
226+
Method getParseMethod() { result = this.getAMethod("Parse") }
227+
228+
/** Gets the `SelectToken` method. */
229+
Method getSelectTokenMethod() { result = this.getABaseType*().getAMethod("SelectToken") }
230+
}
231+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// semmle-extractor-options: ${testdir}/../../../resources/stubs/JsonNET.cs /r:System.Linq.dll
2+
3+
using Newtonsoft.Json;
4+
using Newtonsoft.Json.Linq;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
8+
namespace JsonTest
9+
{
10+
class Dataflow
11+
{
12+
void Sink(object o)
13+
{
14+
}
15+
16+
void F()
17+
{
18+
string t = "tainted";
19+
string u = "untainted";
20+
21+
Sink(JsonConvert.ToString(int.Parse(t)));
22+
23+
var taintedObject = JsonConvert.DeserializeObject<Object>(t);
24+
Sink(taintedObject);
25+
Sink(taintedObject.tainted);
26+
Sink(taintedObject.untainted);
27+
Sink(JsonConvert.SerializeObject(taintedObject));
28+
Sink(taintedObject.taintedValues["1"]);
29+
Sink(taintedObject.taintedArray[0]);
30+
31+
var taintedObject2 = JsonConvert.DeserializeObject<Object2>(t);
32+
Sink(taintedObject2.tainted);
33+
Sink(taintedObject2.untainted);
34+
35+
Object taintedPopulatedObject = new Object();
36+
JsonConvert.PopulateObject(t, taintedPopulatedObject);
37+
Sink(taintedPopulatedObject.tainted); // False negative
38+
39+
Object untaintedObject = JsonConvert.DeserializeObject<Object>(u);
40+
Sink(untaintedObject);
41+
42+
// JObject tests
43+
var jobject = JObject.Parse(t);
44+
Sink(jobject);
45+
Sink(jobject["1"]);
46+
Sink(jobject["1"]["2"]);
47+
Sink((string)jobject["1"]["2"]);
48+
49+
// Linq JToken tests
50+
Sink(jobject.First(i => true));
51+
Sink(jobject["2"].First(i => true));
52+
Sink(jobject["2"]["3"].First(i => true));
53+
Sink(jobject.SelectToken("Manufacturers[0].Name"));
54+
55+
JObject untaintedJObject = JObject.Parse(u);
56+
Sink(untaintedJObject);
57+
Sink(untaintedJObject.First(i => true));
58+
}
59+
60+
public class Object
61+
{
62+
public int tainted;
63+
64+
[JsonIgnore]
65+
public int untainted;
66+
67+
public Dictionary<string,string> taintedValues;
68+
69+
public string[] taintedArray;
70+
}
71+
72+
[JsonObject(MemberSerialization.OptIn)]
73+
public class Object2
74+
{
75+
public int untainted;
76+
77+
[JsonRequired]
78+
public int tainted;
79+
}
80+
}
81+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:21:18:21:51 | call to method ToString |
2+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:24:18:24:30 | access to local variable taintedObject |
3+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:25:18:25:38 | (...) ... |
4+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:27:18:27:59 | call to method SerializeObject |
5+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:28:18:28:49 | access to indexer |
6+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:29:18:29:46 | access to array element |
7+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:32:18:32:39 | (...) ... |
8+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:44:18:44:24 | access to local variable jobject |
9+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:45:18:45:29 | access to indexer |
10+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:46:18:46:34 | access to indexer |
11+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:47:18:47:42 | call to operator explicit conversion |
12+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:50:18:50:41 | call to method First |
13+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:51:18:51:46 | call to method First |
14+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:52:18:52:51 | call to method First |
15+
| Json.cs:18:24:18:32 | "tainted" | Json.cs:53:18:53:61 | call to method SelectToken |
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import csharp
2+
import semmle.code.csharp.dataflow.TaintTracking
3+
4+
class Configuration extends TaintTracking::Configuration {
5+
Configuration() { this = "Json.NET test" }
6+
7+
override predicate isSource(DataFlow::Node src) {
8+
src.asExpr().(StringLiteral).getValue() = "tainted"
9+
}
10+
11+
override predicate isSink(DataFlow::Node sink) {
12+
exists(MethodCall c | c.getArgument(0) = sink.asExpr() and c.getTarget().getName() = "Sink")
13+
}
14+
}
15+
16+
from Configuration c, DataFlow::Node source, DataFlow::Node sink
17+
where c.hasFlow(source, sink)
18+
select source, sink

0 commit comments

Comments
 (0)