Skip to content

Commit f7a0b17

Browse files
authored
Merge pull request github#7687 from yoff/python/PathInjection-FlowState
python: Rewrite path injection query to use flow state
2 parents 8df04c5 + 0c3bce1 commit f7a0b17

File tree

9 files changed

+288
-144
lines changed

9 files changed

+288
-144
lines changed

python/ql/lib/semmle/python/Concepts.qll

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ module Path {
115115
PathNormalization::Range range;
116116

117117
PathNormalization() { this = range }
118+
119+
/** Gets an argument to this path normalization that is interpreted as a path. */
120+
DataFlow::Node getPathArg() { result = range.getPathArg() }
118121
}
119122

120123
/** Provides a class for modeling new path normalization APIs. */
@@ -123,7 +126,10 @@ module Path {
123126
* A data-flow node that performs path normalization. This is often needed in order
124127
* to safely access paths.
125128
*/
126-
abstract class Range extends DataFlow::Node { }
129+
abstract class Range extends DataFlow::Node {
130+
/** Gets an argument to this path normalization that is interpreted as a path. */
131+
abstract DataFlow::Node getPathArg();
132+
}
127133
}
128134

129135
/** A data-flow node that checks that a path is safe to access. */

python/ql/lib/semmle/python/frameworks/Stdlib.qll

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -970,7 +970,7 @@ private module StdlibPrivate {
970970
private class OsPathNormpathCall extends Path::PathNormalization::Range, DataFlow::CallCfgNode {
971971
OsPathNormpathCall() { this = os::path().getMember("normpath").getACall() }
972972

973-
DataFlow::Node getPathArg() { result in [this.getArg(0), this.getArgByName("path")] }
973+
override DataFlow::Node getPathArg() { result in [this.getArg(0), this.getArgByName("path")] }
974974
}
975975

976976
/**
@@ -980,7 +980,7 @@ private module StdlibPrivate {
980980
private class OsPathAbspathCall extends Path::PathNormalization::Range, DataFlow::CallCfgNode {
981981
OsPathAbspathCall() { this = os::path().getMember("abspath").getACall() }
982982

983-
DataFlow::Node getPathArg() { result in [this.getArg(0), this.getArgByName("path")] }
983+
override DataFlow::Node getPathArg() { result in [this.getArg(0), this.getArgByName("path")] }
984984
}
985985

986986
/**
@@ -990,7 +990,7 @@ private module StdlibPrivate {
990990
private class OsPathRealpathCall extends Path::PathNormalization::Range, DataFlow::CallCfgNode {
991991
OsPathRealpathCall() { this = os::path().getMember("realpath").getACall() }
992992

993-
DataFlow::Node getPathArg() { result in [this.getArg(0), this.getArgByName("path")] }
993+
override DataFlow::Node getPathArg() { result in [this.getArg(0), this.getArgByName("path")] }
994994
}
995995

996996
/**

python/ql/lib/semmle/python/security/dataflow/ChainedConfigs12.qll

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/**
2+
* DEPRECATED -- use flow state instead
3+
*
24
* This defines a `PathGraph` where sinks from `TaintTracking::Configuration`s are identified with
35
* sources from `TaintTracking2::Configuration`s if they represent the same `ControlFlowNode`.
46
*
@@ -28,9 +30,11 @@ private newtype TCustomPathNode =
2830
CrossoverNode(DataFlow::Node node) { crossoverNode(node) }
2931

3032
/**
33+
* DEPRECATED: Use flow state instead
34+
*
3135
* A class representing the set of all the path nodes in either config.
3236
*/
33-
class CustomPathNode extends TCustomPathNode {
37+
deprecated class CustomPathNode extends TCustomPathNode {
3438
/** Gets the PathNode if it is in Config1. */
3539
DataFlow::PathNode asNode1() {
3640
this = Config1Node(result) or this = CrossoverNode(result.getNode())
@@ -64,17 +68,25 @@ class CustomPathNode extends TCustomPathNode {
6468
}
6569
}
6670

67-
/** Holds if `(a,b)` is an edge in the graph of data flow path explanations. */
68-
query predicate edges(CustomPathNode a, CustomPathNode b) {
71+
/**
72+
* DEPRECATED: Use flow state instead
73+
*
74+
* Holds if `(a,b)` is an edge in the graph of data flow path explanations.
75+
*/
76+
deprecated query predicate edges(CustomPathNode a, CustomPathNode b) {
6977
// Edge is in Config1 graph
7078
DataFlow::PathGraph::edges(a.asNode1(), b.asNode1())
7179
or
7280
// Edge is in Config2 graph
7381
DataFlow2::PathGraph::edges(a.asNode2(), b.asNode2())
7482
}
7583

76-
/** Holds if `n` is a node in the graph of data flow path explanations. */
77-
query predicate nodes(CustomPathNode n, string key, string val) {
84+
/**
85+
* DEPRECATED: Use flow state instead
86+
*
87+
* Holds if `n` is a node in the graph of data flow path explanations.
88+
*/
89+
deprecated query predicate nodes(CustomPathNode n, string key, string val) {
7890
// Node is in Config1 graph
7991
DataFlow::PathGraph::nodes(n.asNode1(), key, val)
8092
or

python/ql/lib/semmle/python/security/dataflow/PathInjection.qll

Lines changed: 108 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,102 @@
22
* Provides taint-tracking configurations for detecting "path injection" vulnerabilities.
33
*
44
* Note, for performance reasons: only import this file if
5-
* the Configurations or the `pathInjection` predicate are needed, otherwise
5+
* `PathInjection::Configuration` is needed, otherwise
66
* `PathInjectionCustomizations` should be imported instead.
77
*/
88

99
private import python
1010
private import semmle.python.Concepts
11-
private import semmle.python.dataflow.new.DataFlow
11+
import semmle.python.dataflow.new.DataFlow
12+
import semmle.python.dataflow.new.TaintTracking
13+
14+
/**
15+
* Provides a taint-tracking configuration for detecting "path injection" vulnerabilities.
16+
*/
17+
module PathInjection {
18+
import PathInjectionCustomizations::PathInjection
19+
20+
/**
21+
* A taint-tracking configuration for detecting "path injection" vulnerabilities.
22+
*
23+
* This configuration uses two flow states, `NotNormalized` and `NormalizedUnchecked`,
24+
* to track the requirement that a file path must be first normalized and then checked
25+
* before it is safe to use.
26+
*
27+
* At sources, paths are assumed not normalized. At normalization points, they change
28+
* state to `NormalizedUnchecked` after which they can be made safe by an appropriate
29+
* check of the prefix.
30+
*
31+
* Such checks are ineffective in the `NotNormalized` state.
32+
*/
33+
class Configuration extends TaintTracking::Configuration {
34+
Configuration() { this = "PathInjection" }
35+
36+
override predicate isSource(DataFlow::Node source, DataFlow::FlowState state) {
37+
source instanceof Source and state instanceof NotNormalized
38+
}
39+
40+
override predicate isSink(DataFlow::Node sink, DataFlow::FlowState state) {
41+
sink instanceof Sink and
42+
(
43+
state instanceof NotNormalized or
44+
state instanceof NormalizedUnchecked
45+
)
46+
}
47+
48+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
49+
50+
override predicate isBarrier(DataFlow::Node node, DataFlow::FlowState state) {
51+
// Block `NotNormalized` paths here, since they change state to `NormalizedUnchecked`
52+
node instanceof Path::PathNormalization and
53+
state instanceof NotNormalized
54+
or
55+
node = any(Path::SafeAccessCheck c).getAGuardedNode() and
56+
state instanceof NormalizedUnchecked
57+
}
58+
59+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
60+
guard instanceof SanitizerGuard
61+
}
62+
63+
override predicate isAdditionalFlowStep(
64+
DataFlow::Node nodeFrom, DataFlow::FlowState stateFrom, DataFlow::Node nodeTo,
65+
DataFlow::FlowState stateTo
66+
) {
67+
nodeFrom = nodeTo.(Path::PathNormalization).getPathArg() and
68+
stateFrom instanceof NotNormalized and
69+
stateTo instanceof NormalizedUnchecked
70+
}
71+
}
72+
73+
/** A state signifying that the file path has not been normalized. */
74+
class NotNormalized extends DataFlow::FlowState {
75+
NotNormalized() { this = "NotNormalized" }
76+
}
77+
78+
/** A state signifying that the file path has been normalized, but not checked. */
79+
class NormalizedUnchecked extends DataFlow::FlowState {
80+
NormalizedUnchecked() { this = "NormalizedUnchecked" }
81+
}
82+
}
83+
84+
// ---------------------------------------------------------------------------
85+
// Old, deprecated code
86+
// ---------------------------------------------------------------------------
1287
private import semmle.python.dataflow.new.DataFlow2
13-
private import semmle.python.dataflow.new.TaintTracking
1488
private import semmle.python.dataflow.new.TaintTracking2
15-
import ChainedConfigs12
89+
private import ChainedConfigs12
1690
import PathInjectionCustomizations::PathInjection
1791

1892
// ---------------------------------------------------------------------------
1993
// Case 1. The path is never normalized.
2094
// ---------------------------------------------------------------------------
21-
/** Configuration to find paths from sources to sinks that contain no normalization. */
22-
class PathNotNormalizedConfiguration extends TaintTracking::Configuration {
95+
/**
96+
* DEPRECATED: Use `PathInjection::Configuration` instead
97+
*
98+
* Configuration to find paths from sources to sinks that contain no normalization.
99+
*/
100+
deprecated class PathNotNormalizedConfiguration extends TaintTracking::Configuration {
23101
PathNotNormalizedConfiguration() { this = "PathNotNormalizedConfiguration" }
24102

25103
override predicate isSource(DataFlow::Node source) { source instanceof Source }
@@ -38,18 +116,24 @@ class PathNotNormalizedConfiguration extends TaintTracking::Configuration {
38116
}
39117

40118
/**
119+
* DEPRECATED: Use `PathInjection::Configuration` instead
120+
*
41121
* Holds if there is a path injection from source to sink, where the (python) path is
42122
* not normalized.
43123
*/
44-
predicate pathNotNormalized(CustomPathNode source, CustomPathNode sink) {
124+
deprecated predicate pathNotNormalized(CustomPathNode source, CustomPathNode sink) {
45125
any(PathNotNormalizedConfiguration config).hasFlowPath(source.asNode1(), sink.asNode1())
46126
}
47127

48128
// ---------------------------------------------------------------------------
49129
// Case 2. The path is normalized at least once, but never checked afterwards.
50130
// ---------------------------------------------------------------------------
51-
/** Configuration to find paths from sources to normalizations that contain no prior normalizations. */
52-
class FirstNormalizationConfiguration extends TaintTracking::Configuration {
131+
/**
132+
* DEPRECATED: Use `PathInjection::Configuration` instead
133+
*
134+
* Configuration to find paths from sources to normalizations that contain no prior normalizations.
135+
*/
136+
deprecated class FirstNormalizationConfiguration extends TaintTracking::Configuration {
53137
FirstNormalizationConfiguration() { this = "FirstNormalizationConfiguration" }
54138

55139
override predicate isSource(DataFlow::Node source) { source instanceof Source }
@@ -65,8 +149,12 @@ class FirstNormalizationConfiguration extends TaintTracking::Configuration {
65149
}
66150
}
67151

68-
/** Configuration to find paths from normalizations to sinks that do not go through a check. */
69-
class NormalizedPathNotCheckedConfiguration extends TaintTracking2::Configuration {
152+
/**
153+
* DEPRECATED: Use `PathInjection::Configuration` instead
154+
*
155+
* Configuration to find paths from normalizations to sinks that do not go through a check.
156+
*/
157+
deprecated class NormalizedPathNotCheckedConfiguration extends TaintTracking2::Configuration {
70158
NormalizedPathNotCheckedConfiguration() { this = "NormalizedPathNotCheckedConfiguration" }
71159

72160
override predicate isSource(DataFlow::Node source) { source instanceof Path::PathNormalization }
@@ -83,10 +171,12 @@ class NormalizedPathNotCheckedConfiguration extends TaintTracking2::Configuratio
83171
}
84172

85173
/**
174+
* DEPRECATED: Use `PathInjection::Configuration` instead
175+
*
86176
* Holds if there is a path injection from source to sink, where the (python) path is
87177
* normalized at least once, but never checked afterwards.
88178
*/
89-
predicate pathNotCheckedAfterNormalization(CustomPathNode source, CustomPathNode sink) {
179+
deprecated predicate pathNotCheckedAfterNormalization(CustomPathNode source, CustomPathNode sink) {
90180
exists(
91181
FirstNormalizationConfiguration config, DataFlow::PathNode mid1, DataFlow2::PathNode mid2,
92182
NormalizedPathNotCheckedConfiguration config2
@@ -100,8 +190,12 @@ predicate pathNotCheckedAfterNormalization(CustomPathNode source, CustomPathNode
100190
// ---------------------------------------------------------------------------
101191
// Query: Either case 1 or case 2.
102192
// ---------------------------------------------------------------------------
103-
/** Holds if there is a path injection from source to sink */
104-
predicate pathInjection(CustomPathNode source, CustomPathNode sink) {
193+
/**
194+
* DEPRECATED: Use `PathInjection::Configuration` instead
195+
*
196+
* Holds if there is a path injection from source to sink
197+
*/
198+
deprecated predicate pathInjection(CustomPathNode source, CustomPathNode sink) {
105199
pathNotNormalized(source, sink)
106200
or
107201
pathNotCheckedAfterNormalization(source, sink)

python/ql/src/Security/CWE-022/PathInjection.ql

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
import python
2020
import semmle.python.security.dataflow.PathInjection
21+
import DataFlow::PathGraph
2122

22-
from CustomPathNode source, CustomPathNode sink
23-
where pathInjection(source, sink)
24-
select sink, source, sink, "This path depends on $@.", source, "a user-provided value"
23+
from PathInjection::Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
24+
where config.hasFlowPath(source, sink)
25+
select sink.getNode(), source, sink, "This path depends on $@.", source.getNode(),
26+
"a user-provided value"

python/ql/src/experimental/Security/CWE-943/NoSQLInjection.ql

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111

1212
import python
1313
import experimental.semmle.python.security.injection.NoSQLInjection
14+
import DataFlow::PathGraph
1415

15-
from CustomPathNode source, CustomPathNode sink
16-
where noSQLInjectionFlow(source, sink)
16+
from NoSQLInjection::Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
17+
where config.hasFlowPath(source, sink)
1718
select sink, source, sink, "$@ NoSQL query contains an unsanitized $@", sink, "This", source,
1819
"user-provided value"

0 commit comments

Comments
 (0)