Skip to content

Commit e5abaa7

Browse files
authored
Merge pull request #2585 from calumgrant/cs/serialization-check-bypass
C#: Improvements to cs/serialization-check-bypass
2 parents a91f10f + 6790028 commit e5abaa7

File tree

9 files changed

+310
-7
lines changed

9 files changed

+310
-7
lines changed

csharp/ql/src/Security Features/CWE-020/RuntimeChecksBypass.qhelp

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,40 @@
55

66
<overview>
77
<p>
8-
A write that looks like it may be bypassing runtime checks.
8+
Fields that are deserialized should be validated, otherwise the deserialized object
9+
could contain invalid data.
10+
</p>
11+
12+
<p>
13+
This query finds cases where a field is validated in a constructor, but not in a deserialization method.
14+
This is an indication that the deserialization method is missing a validation step.
915
</p>
1016

1117
</overview>
18+
19+
<recommendation>
20+
<p>
21+
If a field needs to be validated, then ensure that validation is also performed during deserialization.
22+
</p>
23+
</recommendation>
24+
25+
<example>
26+
27+
<p>
28+
The following example has the validation of the <code>Age</code> field in the constructor
29+
but not in the deserialization method:
30+
</p>
31+
32+
<sample src="RuntimeChecksBypassBad.cs" />
33+
34+
<p>
35+
The problem is fixed by adding validation to the deserialization method as follows:
36+
</p>
37+
38+
<sample src="RuntimeChecksBypassGood.cs" />
39+
40+
</example>
41+
1242
<references>
1343

1444
<li>

csharp/ql/src/Security Features/CWE-020/RuntimeChecksBypass.ql

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,31 @@
1313
*/
1414

1515
import semmle.code.csharp.serialization.Serialization
16+
import semmle.code.csharp.controlflow.Guards
1617

1718
/**
1819
* The result is a write to the field `f`, assigning it the value
1920
* of variable `v` which was checked by the condition `check`.
2021
*/
21-
Expr checkedWrite(Field f, Variable v, IfStmt check) {
22+
GuardedExpr checkedWrite(Field f, Variable v, IfStmt check) {
2223
result = v.getAnAccess() and
2324
result = f.getAnAssignedValue() and
24-
check.getCondition() = v.getAnAccess().getParent*() and
25-
result.getAControlFlowNode() = check.getAControlFlowNode().getASuccessor*()
25+
check.getCondition().getAChildExpr*() = result.getAGuard(_, _)
26+
}
27+
28+
/**
29+
* The result is an unsafe write to the field `f`, where
30+
* there is no check performed within the (calling) scope of the method.
31+
*/
32+
Expr uncheckedWrite(Callable callable, Field f) {
33+
result = f.getAnAssignedValue() and
34+
result.getEnclosingCallable() = callable and
35+
not callable.calls*(checkedWrite(f, _, _).getEnclosingCallable())
2636
}
2737

2838
from BinarySerializableType t, Field f, IfStmt check, Expr write, Expr unsafeWrite
2939
where
3040
f = t.getASerializedField() and
3141
write = checkedWrite(f, t.getAConstructor().getAParameter(), check) and
32-
unsafeWrite = f.getAnAssignedValue() and
33-
t.getADeserializationCallback() = unsafeWrite.getEnclosingCallable() and
34-
not t.getADeserializationCallback().calls*(checkedWrite(f, _, _).getEnclosingCallable())
42+
unsafeWrite = uncheckedWrite(t.getADeserializationCallback(), f)
3543
select unsafeWrite, "This write to $@ may be circumventing a $@.", f, f.toString(), check, "check"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
4+
[Serializable]
5+
public class PersonBad : ISerializable
6+
{
7+
public int Age;
8+
9+
public PersonBad(int age)
10+
{
11+
if (age < 0)
12+
throw new ArgumentException(nameof(age));
13+
Age = age;
14+
}
15+
16+
[OnDeserializing]
17+
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
18+
{
19+
Age = info.GetInt32("age"); // BAD - write is unsafe
20+
}
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
4+
[Serializable]
5+
public class PersonGood : ISerializable
6+
{
7+
public int Age;
8+
9+
public PersonGood(int age)
10+
{
11+
if (age < 0)
12+
throw new ArgumentException(nameof(age));
13+
Age = age;
14+
}
15+
16+
[OnDeserializing]
17+
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
18+
{
19+
int age = info.GetInt32("age");
20+
if (age < 0)
21+
throw new SerializationException(nameof(Age));
22+
Age = age; // GOOD - write is safe
23+
}
24+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
4+
[Serializable]
5+
public class Test1
6+
{
7+
public string f;
8+
9+
public Test1(string v)
10+
{
11+
if (v == "valid")
12+
{
13+
f = v /* safe write */;
14+
}
15+
}
16+
17+
[OnDeserializing]
18+
public void Deserialize()
19+
{
20+
f = "invalid" /* unsafe write */;
21+
}
22+
}
23+
24+
[Serializable]
25+
public class Test2
26+
{
27+
public string f;
28+
29+
public Test2(string v)
30+
{
31+
if (v == "valid")
32+
{
33+
f = v /* safe write */;
34+
}
35+
}
36+
37+
[OnDeserializing]
38+
public void Deserialize()
39+
{
40+
var v = "invalid";
41+
f = v /* unsafe write -- false negative */;
42+
43+
if (v == "valid")
44+
{
45+
f = v; /* safe write */
46+
}
47+
}
48+
}
49+
50+
[Serializable]
51+
public class Test3
52+
{
53+
public string f;
54+
55+
public Test3(string v)
56+
{
57+
if (v == "valid")
58+
{
59+
f = v /* safe write */;
60+
}
61+
}
62+
63+
[OnDeserializing]
64+
public void Deserialize()
65+
{
66+
var v = "invalid";
67+
f = v /* unsafe write -- false negative */;
68+
Assign(v);
69+
}
70+
71+
private void Assign(string v)
72+
{
73+
f = v /* unsafe write -- false negative */;
74+
75+
if (v == "valid")
76+
{
77+
f = v /* safe write */;
78+
}
79+
}
80+
}
81+
82+
[Serializable]
83+
public class Test4
84+
{
85+
public string f;
86+
87+
public Test4(string v)
88+
{
89+
if (v == "valid")
90+
{
91+
f = v /* safe write */;
92+
}
93+
}
94+
95+
[OnDeserializing]
96+
public void Deserialize()
97+
{
98+
var v = "invalid";
99+
if (v == "valid")
100+
Assign(v);
101+
}
102+
103+
private void Assign(string v)
104+
{
105+
f = v /* safe write */;
106+
}
107+
}
108+
109+
[Serializable]
110+
public class Test5 : ISerializable
111+
{
112+
public int Age;
113+
114+
public Test5(int age)
115+
{
116+
if (age < 0)
117+
throw new ArgumentException(nameof(age));
118+
Age = age /* safe write */;
119+
}
120+
121+
[OnDeserializing]
122+
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
123+
{
124+
Age = info.GetInt32("age"); /* unsafe write */;
125+
}
126+
}
127+
128+
[Serializable]
129+
public class Test6 : ISerializable
130+
{
131+
public int Age;
132+
133+
public Test6(int age)
134+
{
135+
if (age < 0)
136+
throw new ArgumentException(nameof(age));
137+
Age = age /* safe write */;
138+
}
139+
140+
[OnDeserializing]
141+
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
142+
{
143+
int age = info.GetInt32("age");
144+
if (age < 0)
145+
throw new SerializationException("age");
146+
Age = age; /* safe write */;
147+
}
148+
}
149+
150+
[Serializable]
151+
public class Test7 : ISerializable
152+
{
153+
public int Age;
154+
155+
public Test7(int age)
156+
{
157+
if (age < 0)
158+
throw new ArgumentException(nameof(age));
159+
Age = age /* safe write */;
160+
}
161+
162+
[OnDeserializing]
163+
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
164+
{
165+
int age = info.GetInt32("age");
166+
if (false)
167+
throw new SerializationException("age");
168+
Age = age; /* unsafe write */;
169+
}
170+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
4+
[Serializable]
5+
public class PersonBad : ISerializable
6+
{
7+
public int Age;
8+
9+
public PersonBad(int age)
10+
{
11+
if (age < 0)
12+
throw new ArgumentException(nameof(age));
13+
Age = age;
14+
}
15+
16+
[OnDeserializing]
17+
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
18+
{
19+
Age = info.GetInt32("age"); // BAD - write is unsafe
20+
}
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
4+
[Serializable]
5+
public class PersonGood : ISerializable
6+
{
7+
public int Age;
8+
9+
public PersonGood(int age)
10+
{
11+
if (age < 0)
12+
throw new ArgumentException(nameof(age));
13+
Age = age;
14+
}
15+
16+
[OnDeserializing]
17+
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
18+
{
19+
int age = info.GetInt32("age");
20+
if (age < 0)
21+
throw new SerializationException(nameof(Age));
22+
Age = age; // GOOD - write is safe
23+
}
24+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
| RuntimeChecksBypass.cs:20:13:20:21 | "invalid" | This write to $@ may be circumventing a $@. | RuntimeChecksBypass.cs:7:19:7:19 | f | f | RuntimeChecksBypass.cs:11:9:14:9 | if (...) ... | check |
2+
| RuntimeChecksBypass.cs:124:15:124:34 | call to method GetInt32 | This write to $@ may be circumventing a $@. | RuntimeChecksBypass.cs:112:16:112:18 | Age | Age | RuntimeChecksBypass.cs:116:9:117:53 | if (...) ... | check |
3+
| RuntimeChecksBypass.cs:168:15:168:17 | access to local variable age | This write to $@ may be circumventing a $@. | RuntimeChecksBypass.cs:153:16:153:18 | Age | Age | RuntimeChecksBypass.cs:157:9:158:53 | if (...) ... | check |
4+
| RuntimeChecksBypassBad.cs:19:15:19:34 | call to method GetInt32 | This write to $@ may be circumventing a $@. | RuntimeChecksBypassBad.cs:7:16:7:18 | Age | Age | RuntimeChecksBypassBad.cs:11:9:12:53 | if (...) ... | check |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security Features/CWE-020/RuntimeChecksBypass.ql

0 commit comments

Comments
 (0)