|
14 | 14 | * external/cwe/cwe-036 |
15 | 15 | * external/cwe/cwe-073 |
16 | 16 | * external/cwe/cwe-099 |
| 17 | + * |
| 18 | + * The query detects cases where a user-controlled path is used in an unsafe manner, |
| 19 | + * meaning it is not both normalized and _afterwards_ checked. |
| 20 | + * |
| 21 | + * It does so by dividing the problematic situation into two cases: |
| 22 | + * 1. The file path is never normalized. |
| 23 | + * This is easily detected by using normalization as a sanitizer. |
| 24 | + * |
| 25 | + * 2. The file path is normalized at least once, but never checked afterwards. |
| 26 | + * This is detected by finding the earliest normalization and then ensuring that |
| 27 | + * no checks happen later. Since we start from the earliest normalization, |
| 28 | + * we know that the absence of checks means that no normalization has a |
| 29 | + * check after it. (No checks after a second normalization would be ok if |
| 30 | + * there was a check between the first and the second.) |
| 31 | + * |
| 32 | + * Note that one could make the dual split on whether the file path is ever checked. This does |
| 33 | + * not work as nicely, however, since checking is modelled as a `BarrierGuard` rather than |
| 34 | + * as a `Sanitizer`. That means that only some dataflow paths out of a check will be removed, |
| 35 | + * and so identifying the last check is not possible simply by finding a dataflow path from it |
| 36 | + * to a sink. |
17 | 37 | */ |
18 | 38 |
|
19 | 39 | import python |
20 | | -import semmle.python.security.Paths |
21 | | -/* Sources */ |
22 | | -import semmle.python.web.HttpRequest |
23 | | -/* Sinks */ |
24 | | -import semmle.python.security.injection.Path |
| 40 | +import experimental.dataflow.DataFlow |
| 41 | +import experimental.dataflow.DataFlow2 |
| 42 | +import experimental.dataflow.TaintTracking |
| 43 | +import experimental.dataflow.TaintTracking2 |
| 44 | +import experimental.semmle.python.Concepts |
| 45 | +import experimental.dataflow.RemoteFlowSources |
| 46 | +import ChainedConfigs12 |
25 | 47 |
|
26 | | -class PathInjectionConfiguration extends TaintTracking::Configuration { |
27 | | - PathInjectionConfiguration() { this = "Path injection configuration" } |
| 48 | +// --------------------------------------------------------------------------- |
| 49 | +// Case 1. The path is never normalized. |
| 50 | +// --------------------------------------------------------------------------- |
| 51 | +/** Configuration to find paths from sources to sinks that contain no normalization. */ |
| 52 | +class PathNotNormalizedConfiguration extends TaintTracking::Configuration { |
| 53 | + PathNotNormalizedConfiguration() { this = "PathNotNormalizedConfiguration" } |
28 | 54 |
|
29 | | - override predicate isSource(TaintTracking::Source source) { |
30 | | - source instanceof HttpRequestTaintSource |
| 55 | + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } |
| 56 | + |
| 57 | + override predicate isSink(DataFlow::Node sink) { |
| 58 | + sink = any(FileSystemAccess e).getAPathArgument() |
31 | 59 | } |
32 | 60 |
|
33 | | - override predicate isSink(TaintTracking::Sink sink) { sink instanceof OpenNode } |
| 61 | + override predicate isSanitizer(DataFlow::Node node) { node instanceof Path::PathNormalization } |
| 62 | +} |
| 63 | + |
| 64 | +predicate pathNotNormalized(CustomPathNode source, CustomPathNode sink) { |
| 65 | + any(PathNotNormalizedConfiguration config).hasFlowPath(source.asNode1(), sink.asNode1()) |
| 66 | +} |
| 67 | + |
| 68 | +// --------------------------------------------------------------------------- |
| 69 | +// Case 2. The path is normalized at least once, but never checked afterwards. |
| 70 | +// --------------------------------------------------------------------------- |
| 71 | +/** Configuration to find paths from sources to normalizations that contain no prior normalizations. */ |
| 72 | +class FirstNormalizationConfiguration extends TaintTracking::Configuration { |
| 73 | + FirstNormalizationConfiguration() { this = "FirstNormalizationConfiguration" } |
| 74 | + |
| 75 | + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } |
| 76 | + |
| 77 | + override predicate isSink(DataFlow::Node sink) { sink instanceof Path::PathNormalization } |
34 | 78 |
|
35 | | - override predicate isSanitizer(Sanitizer sanitizer) { |
36 | | - sanitizer instanceof PathSanitizer or |
37 | | - sanitizer instanceof NormalizedPathSanitizer |
| 79 | + override predicate isSanitizerOut(DataFlow::Node node) { node instanceof Path::PathNormalization } |
| 80 | +} |
| 81 | + |
| 82 | +/** Configuration to find paths from normalizations to sinks that do not go through a check. */ |
| 83 | +class NormalizedPathNotCheckedConfiguration extends TaintTracking2::Configuration { |
| 84 | + NormalizedPathNotCheckedConfiguration() { this = "NormalizedPathNotCheckedConfiguration" } |
| 85 | + |
| 86 | + override predicate isSource(DataFlow::Node source) { source instanceof Path::PathNormalization } |
| 87 | + |
| 88 | + override predicate isSink(DataFlow::Node sink) { |
| 89 | + sink = any(FileSystemAccess e).getAPathArgument() |
38 | 90 | } |
39 | 91 |
|
40 | | - override predicate isExtension(TaintTracking::Extension extension) { |
41 | | - extension instanceof AbsPath |
| 92 | + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { |
| 93 | + guard instanceof Path::SafeAccessCheck |
42 | 94 | } |
43 | 95 | } |
44 | 96 |
|
45 | | -from PathInjectionConfiguration config, TaintedPathSource src, TaintedPathSink sink |
46 | | -where config.hasFlowPath(src, sink) |
47 | | -select sink.getSink(), src, sink, "This path depends on $@.", src.getSource(), |
48 | | - "a user-provided value" |
| 97 | +predicate pathNotCheckedAfterNormalization(CustomPathNode source, CustomPathNode sink) { |
| 98 | + exists( |
| 99 | + FirstNormalizationConfiguration config, DataFlow::PathNode mid1, DataFlow2::PathNode mid2, |
| 100 | + NormalizedPathNotCheckedConfiguration config2 |
| 101 | + | |
| 102 | + config.hasFlowPath(source.asNode1(), mid1) and |
| 103 | + config2.hasFlowPath(mid2, sink.asNode2()) and |
| 104 | + mid1.getNode().asCfgNode() = mid2.getNode().asCfgNode() |
| 105 | + ) |
| 106 | +} |
| 107 | + |
| 108 | +// --------------------------------------------------------------------------- |
| 109 | +// Query: Either case 1 or case 2. |
| 110 | +// --------------------------------------------------------------------------- |
| 111 | +from CustomPathNode source, CustomPathNode sink |
| 112 | +where |
| 113 | + pathNotNormalized(source, sink) |
| 114 | + or |
| 115 | + pathNotCheckedAfterNormalization(source, sink) |
| 116 | +select sink, source, sink, "This path depends on $@.", source, "a user-provided value" |
0 commit comments