Skip to content

Commit 1cc5e54

Browse files
committed
Python: Add SSRF queries
I've added 2 queries: - one that detects full SSRF, where an attacker can control the full URL, which is always bad - and one for partial SSRF, where an attacker can control parts of an URL (such as the path, query parameters, or fragment), which is not a big problem in many cases (but might still be exploitable) full SSRF should run by default, and partial SSRF should not (but makes it easy to see the other results). Some elements of the full SSRF queries needs a bit more polishing, like being able to detect `"https://" + user_input` is in fact controlling the full URL.
1 parent 579de0c commit 1cc5e54

12 files changed

+648
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Provides a taint-tracking configuration for detecting "Server-side request forgery" vulnerabilities.
3+
*
4+
* Note, for performance reasons: only import this file if
5+
* `ServerSideRequestForgery::Configuration` is needed, otherwise
6+
* `ServerSideRequestForgeryCustomizations` should be imported instead.
7+
*/
8+
9+
private import python
10+
import semmle.python.dataflow.new.DataFlow
11+
import semmle.python.dataflow.new.TaintTracking
12+
13+
/**
14+
* Provides a taint-tracking configuration for detecting "Server-side request forgery" vulnerabilities.
15+
*
16+
* This configuration has a sanitizer to limit results to cases where attacker has full control of URL.
17+
* See `PartialServerSideRequestForgery` for a variant without this requirement.
18+
*/
19+
module FullServerSideRequestForgery {
20+
import ServerSideRequestForgeryCustomizations::ServerSideRequestForgery
21+
22+
/**
23+
* A taint-tracking configuration for detecting "Server-side request forgery" vulnerabilities.
24+
*/
25+
class Configuration extends TaintTracking::Configuration {
26+
Configuration() { this = "FullServerSideRequestForgery" }
27+
28+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
29+
30+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
31+
32+
override predicate isSanitizer(DataFlow::Node node) {
33+
node instanceof Sanitizer
34+
or
35+
node instanceof FullUrlControlSanitizer
36+
}
37+
38+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
39+
guard instanceof SanitizerGuard
40+
}
41+
}
42+
}
43+
44+
/**
45+
* Provides a taint-tracking configuration for detecting "Server-side request forgery" vulnerabilities.
46+
*
47+
* This configuration has results, even when the attacker does not have full control over the URL.
48+
* See `FullServerSideRequestForgery` for variant that has this requirement.
49+
*/
50+
module PartialServerSideRequestForgery {
51+
import ServerSideRequestForgeryCustomizations::ServerSideRequestForgery
52+
53+
/**
54+
* A taint-tracking configuration for detecting "Server-side request forgery" vulnerabilities.
55+
*/
56+
class Configuration extends TaintTracking::Configuration {
57+
Configuration() { this = "PartialServerSideRequestForgery" }
58+
59+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
60+
61+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
62+
63+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
64+
65+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
66+
guard instanceof SanitizerGuard
67+
}
68+
}
69+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for detecting
3+
* "Server-side request forgery"
4+
* vulnerabilities, as well as extension points for adding your own.
5+
*/
6+
7+
private import python
8+
private import semmle.python.dataflow.new.DataFlow
9+
private import semmle.python.Concepts
10+
private import semmle.python.dataflow.new.RemoteFlowSources
11+
private import semmle.python.dataflow.new.BarrierGuards
12+
13+
/**
14+
* Provides default sources, sinks and sanitizers for detecting
15+
* "Server-side request forgery"
16+
* vulnerabilities, as well as extension points for adding your own.
17+
*/
18+
module ServerSideRequestForgery {
19+
/**
20+
* A data flow source for "Server-side request forgery" vulnerabilities.
21+
*/
22+
abstract class Source extends DataFlow::Node { }
23+
24+
/**
25+
* A data flow sink for "Server-side request forgery" vulnerabilities.
26+
*/
27+
abstract class Sink extends DataFlow::Node { }
28+
29+
/**
30+
* A sanitizer for "Server-side request forgery" vulnerabilities.
31+
*/
32+
abstract class Sanitizer extends DataFlow::Node { }
33+
34+
/**
35+
* A sanitizer for "Server-side request forgery" vulnerabilities,
36+
* that ensures the attacker does not have full control of the URL. (that is, might
37+
* still be able to control path or query parameters).
38+
*/
39+
abstract class FullUrlControlSanitizer extends DataFlow::Node { }
40+
41+
/**
42+
* A sanitizer guard for "Server-side request forgery" vulnerabilities.
43+
*/
44+
abstract class SanitizerGuard extends DataFlow::BarrierGuard { }
45+
46+
/**
47+
* A source of remote user input, considered as a flow source.
48+
*/
49+
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
50+
51+
/** The URL of an HTTP request, considered as a sink. */
52+
class HttpRequestUrlAsSink extends Sink {
53+
HttpRequestUrlAsSink() {
54+
exists(HTTP::Client::Request req | req.getAUrlPart() = this) and
55+
// Since we find sinks inside stdlib, we need to exclude them manually. See
56+
// comment for command injection sinks for more details.
57+
not this.getScope().getEnclosingModule().getName() in ["http.client", "httplib"]
58+
}
59+
}
60+
61+
/**
62+
* A comparison with a constant string, considered as a sanitizer-guard.
63+
*/
64+
class StringConstCompareAsSanitizerGuard extends SanitizerGuard, StringConstCompare { }
65+
66+
/**
67+
* A string construction (concat, format, f-string) where the left side is not
68+
* user-controlled.
69+
*/
70+
class StringConstructioneAsFullUrlControlSanitizer extends FullUrlControlSanitizer {
71+
StringConstructioneAsFullUrlControlSanitizer() {
72+
// string concat
73+
exists(BinaryExprNode add |
74+
add.getOp() instanceof Add and
75+
add.getRight() = this.asCfgNode()
76+
)
77+
or
78+
// % formatting
79+
exists(BinaryExprNode fmt |
80+
fmt.getOp() instanceof Mod and
81+
fmt.getRight() = this.asCfgNode()
82+
)
83+
or
84+
// arguments to a format call
85+
exists(DataFlow::MethodCallNode call |
86+
call.getMethodName() = "format" and
87+
this in [call.getArg(_), call.getArgByName(_)]
88+
)
89+
or
90+
// f-string
91+
exists(Fstring fstring | fstring.getValue(any(int i | i > 0)) = this.asExpr())
92+
}
93+
}
94+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @name Full server-side request forgery
3+
* @description Making a network request to a URL that is fully user-controlled allows for request forgery attacks.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 9.1
7+
* @precision high
8+
* @id py/full-ssrf
9+
* @tags security
10+
* external/cwe/cwe-918
11+
*/
12+
13+
import python
14+
import semmle.python.security.dataflow.ServerSideRequestForgery
15+
import DataFlow::PathGraph
16+
17+
from
18+
FullServerSideRequestForgery::Configuration config, DataFlow::PathNode source,
19+
DataFlow::PathNode sink
20+
where config.hasFlowPath(source, sink)
21+
select sink.getNode(), source, sink, "The full URL of this request depends on $@.",
22+
source.getNode(), "a user-provided value"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @name Partial server-side request forgery
3+
* @description Making a network request to a URL that is partially user-controlled allows for request forgery attacks.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 9.1
7+
* @precision medium
8+
* @id py/partial-ssrf
9+
* @tags security
10+
* external/cwe/cwe-918
11+
*/
12+
13+
import python
14+
import semmle.python.security.dataflow.ServerSideRequestForgery
15+
import DataFlow::PathGraph
16+
17+
from
18+
FullServerSideRequestForgery::Configuration fullConfig,
19+
PartialServerSideRequestForgery::Configuration partialConfig, DataFlow::PathNode source,
20+
DataFlow::PathNode sink
21+
where
22+
partialConfig.hasFlowPath(source, sink) and
23+
not fullConfig.hasFlow(source.getNode(), sink.getNode())
24+
select sink.getNode(), source, sink, "Part of the URL of this request depends on $@.",
25+
source.getNode(), "a user-provided value"

0 commit comments

Comments
 (0)