Skip to content

Commit 953bd09

Browse files
authored
Merge pull request #106 from microsoft/tainted-path-barrier-with-state
C#: Make `StartsWith` and `EndsWith` sanitizers on normalized paths
2 parents e36e617 + 758196e commit 953bd09

File tree

2 files changed

+76
-12
lines changed

2 files changed

+76
-12
lines changed

csharp/ql/lib/semmle/code/csharp/security/dataflow/TaintedPathQuery.qll

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import csharp
7+
private import codeql.util.Unit
78
private import semmle.code.csharp.controlflow.Guards
89
private import semmle.code.csharp.security.dataflow.flowsinks.FlowSinks
910
private import semmle.code.csharp.security.dataflow.flowsources.FlowSources
@@ -24,23 +25,67 @@ abstract class Sink extends ApiSinkExprNode { }
2425
/**
2526
* A sanitizer for uncontrolled data in path expression vulnerabilities.
2627
*/
27-
abstract class Sanitizer extends DataFlow::ExprNode { }
28+
abstract class Sanitizer extends DataFlow::ExprNode {
29+
/** Holds if this is a sanitizer when the flow state is `state`. */
30+
predicate isBarrier(TaintedPathConfig::FlowState state) { any() }
31+
}
32+
33+
/** A path normalization step. */
34+
private class PathNormalizationStep extends Unit {
35+
/**
36+
* Holds if the flow step from `n1` to `n2` transforms the path into an
37+
* absolute path.
38+
*
39+
* For example, the argument-to-return-value step through a call
40+
* to `System.IO.Path.GetFullPath` is a normalization step.
41+
*/
42+
abstract predicate isAdditionalFlowStep(DataFlow::Node n1, DataFlow::Node n2);
43+
}
44+
45+
private class GetFullPathStep extends PathNormalizationStep {
46+
override predicate isAdditionalFlowStep(DataFlow::Node n1, DataFlow::Node n2) {
47+
exists(Call call |
48+
call.getARuntimeTarget().hasFullyQualifiedName("System.IO.Path", "GetFullPath") and
49+
n1.asExpr() = call.getArgument(0) and
50+
n2.asExpr() = call
51+
)
52+
}
53+
}
2854

2955
/**
3056
* A taint-tracking configuration for uncontrolled data in path expression vulnerabilities.
3157
*/
32-
private module TaintedPathConfig implements DataFlow::ConfigSig {
33-
predicate isSource(DataFlow::Node source) { source instanceof Source }
58+
private module TaintedPathConfig implements DataFlow::StateConfigSig {
59+
newtype FlowState =
60+
additional NotNormalized() or
61+
additional Normalized()
62+
63+
predicate isSource(DataFlow::Node source, FlowState state) {
64+
source instanceof Source and state = NotNormalized()
65+
}
3466

35-
predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
67+
predicate isSink(DataFlow::Node sink, FlowState state) {
68+
sink instanceof Sink and
69+
exists(state)
70+
}
71+
72+
predicate isAdditionalFlowStep(DataFlow::Node n1, FlowState s1, DataFlow::Node n2, FlowState s2) {
73+
any(PathNormalizationStep step).isAdditionalFlowStep(n1, n2) and
74+
s1 = NotNormalized() and
75+
s2 = Normalized()
76+
}
3677

37-
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
78+
predicate isBarrier(DataFlow::Node node, FlowState state) { node.(Sanitizer).isBarrier(state) }
79+
80+
predicate isBarrierOut(DataFlow::Node node, FlowState state) {
81+
isAdditionalFlowStep(node, state, _, _)
82+
}
3883
}
3984

4085
/**
4186
* A taint-tracking module for uncontrolled data in path expression vulnerabilities.
4287
*/
43-
module TaintedPath = TaintTracking::Global<TaintedPathConfig>;
88+
module TaintedPath = TaintTracking::GlobalWithState<TaintedPathConfig>;
4489

4590
/**
4691
* DEPRECATED: Use `ThreatModelSource` instead.
@@ -99,7 +144,7 @@ class StreamWriterTaintedPathSink extends Sink {
99144
}
100145

101146
/**
102-
* A weak guard that is insufficient to prevent path tampering.
147+
* A weak guard that may be insufficient to prevent path tampering.
103148
*/
104149
private class WeakGuard extends Guard {
105150
WeakGuard() {
@@ -118,6 +163,14 @@ private class WeakGuard extends Guard {
118163
or
119164
this.(LogicalOperation).getAnOperand() instanceof WeakGuard
120165
}
166+
167+
predicate isBarrier(TaintedPathConfig::FlowState state) {
168+
state = TaintedPathConfig::Normalized() and
169+
exists(Method m | this.(MethodCall).getTarget() = m |
170+
m.getName() = "StartsWith" or
171+
m.getName() = "EndsWith"
172+
)
173+
}
121174
}
122175

123176
/**
@@ -126,12 +179,17 @@ private class WeakGuard extends Guard {
126179
* A weak check is one that is insufficient to prevent path tampering.
127180
*/
128181
class PathCheck extends Sanitizer {
182+
Guard g;
183+
129184
PathCheck() {
130-
// This expression is structurally replicated in a dominating guard which is not a "weak" check
131-
exists(Guard g, AbstractValues::BooleanValue v |
132-
g = this.(GuardedDataFlowNode).getAGuard(_, v) and
133-
not g instanceof WeakGuard
134-
)
185+
// This expression is structurally replicated in a dominating guard
186+
exists(AbstractValues::BooleanValue v | g = this.(GuardedDataFlowNode).getAGuard(_, v))
187+
}
188+
189+
override predicate isBarrier(TaintedPathConfig::FlowState state) {
190+
g.(WeakGuard).isBarrier(state)
191+
or
192+
not g instanceof WeakGuard
135193
}
136194
}
137195

csharp/ql/test/query-tests/Security Features/CWE-022/TaintedPath/TaintedPath.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ public void ProcessRequest(HttpContext ctx)
5555

5656
// GOOD: A simple type.
5757
File.ReadAllText(int.Parse(path).ToString());
58+
59+
string fullPath = Path.GetFullPath(path);
60+
if (fullPath.StartsWith("C:\\Foo"))
61+
{
62+
File.ReadAllText(fullPath); // GOOD
63+
}
5864
}
5965

6066
public bool IsReusable

0 commit comments

Comments
 (0)