Skip to content

Commit dfc0e9b

Browse files
authored
Merge pull request github#5243 from RasmusWL/port-bind-to-all-interfaces
Python: Port py/bind-socket-all-network-interfaces query
2 parents cb6ee54 + c6d6d07 commit dfc0e9b

File tree

5 files changed

+120
-15
lines changed

5 files changed

+120
-15
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
lgtm,codescanning
2+
* Updated _Binding a socket to all network interfaces_ (`py/bind-socket-all-network-interfaces`) query to use the new type-tracking approach instead of points-to analysis. You may see differences in the results found by the query, but overall this change should result in a more robust and accurate analysis.
3+
* Updated _Binding a socket to all network interfaces_ (`py/bind-socket-all-network-interfaces`) to recognize binding to all interfaces in IPv6 with hostnames `::` and `::0`

python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,86 @@
1111
*/
1212

1313
import python
14+
import semmle.python.dataflow.new.DataFlow
15+
import semmle.python.ApiGraphs
1416

15-
Value aSocket() { result.getClass() = Value::named("socket.socket") }
17+
/** Gets a hostname that can be used to bind to all interfaces. */
18+
private string vulnerableHostname() {
19+
result in [
20+
// IPv4
21+
"0.0.0.0", "",
22+
// IPv6
23+
"::", "::0"
24+
]
25+
}
1626

17-
CallNode socketBindCall() {
18-
result = aSocket().attr("bind").(CallableValue).getACall() and major_version() = 3
27+
/** Gets a reference to a hostname that can be used to bind to all interfaces. */
28+
private DataFlow::LocalSourceNode vulnerableHostnameRef(DataFlow::TypeTracker t, string hostname) {
29+
t.start() and
30+
exists(StrConst allInterfacesStrConst | hostname = vulnerableHostname() |
31+
allInterfacesStrConst.getText() = hostname and
32+
result.asExpr() = allInterfacesStrConst
33+
)
1934
or
20-
result.getFunction().(AttrNode).getObject("bind").pointsTo(aSocket()) and
21-
major_version() = 2
35+
// Due to bad performance when using normal setup with `vulnerableHostnameRef(t2, hostname).track(t2, t)`
36+
// we have inlined that code and forced a join
37+
exists(DataFlow::TypeTracker t2 |
38+
exists(DataFlow::StepSummary summary |
39+
vulnerableHostnameRef_first_join(t2, hostname, result, summary) and
40+
t = t2.append(summary)
41+
)
42+
)
43+
}
44+
45+
pragma[nomagic]
46+
private predicate vulnerableHostnameRef_first_join(
47+
DataFlow::TypeTracker t2, string hostname, DataFlow::Node res, DataFlow::StepSummary summary
48+
) {
49+
DataFlow::StepSummary::step(vulnerableHostnameRef(t2, hostname), res, summary)
2250
}
2351

24-
string allInterfaces() { result = "0.0.0.0" or result = "" }
52+
/** Gets a reference to a hostname that can be used to bind to all interfaces. */
53+
DataFlow::Node vulnerableHostnameRef(string hostname) {
54+
vulnerableHostnameRef(DataFlow::TypeTracker::end(), hostname).flowsTo(result)
55+
}
2556

26-
Value getTextValue(string address) {
27-
result = Value::forUnicode(address) and major_version() = 3
57+
/** Gets a reference to a tuple for which the first element is a hostname that can be used to bind to all interfaces. */
58+
private DataFlow::LocalSourceNode vulnerableAddressTuple(DataFlow::TypeTracker t, string hostname) {
59+
t.start() and
60+
result.asExpr() = any(Tuple tup | tup.getElt(0) = vulnerableHostnameRef(hostname).asExpr())
2861
or
29-
result = Value::forString(address) and major_version() = 2
62+
// Due to bad performance when using normal setup with `vulnerableAddressTuple(t2, hostname).track(t2, t)`
63+
// we have inlined that code and forced a join
64+
exists(DataFlow::TypeTracker t2 |
65+
exists(DataFlow::StepSummary summary |
66+
vulnerableAddressTuple_first_join(t2, hostname, result, summary) and
67+
t = t2.append(summary)
68+
)
69+
)
70+
}
71+
72+
pragma[nomagic]
73+
private predicate vulnerableAddressTuple_first_join(
74+
DataFlow::TypeTracker t2, string hostname, DataFlow::Node res, DataFlow::StepSummary summary
75+
) {
76+
DataFlow::StepSummary::step(vulnerableAddressTuple(t2, hostname), res, summary)
3077
}
3178

32-
from CallNode call, TupleValue args, string address
79+
/** Gets a reference to a tuple for which the first element is a hostname that can be used to bind to all interfaces. */
80+
DataFlow::Node vulnerableAddressTuple(string hostname) {
81+
vulnerableAddressTuple(DataFlow::TypeTracker::end(), hostname).flowsTo(result)
82+
}
83+
84+
/**
85+
* Gets an instance of `socket.socket` using _some_ address family.
86+
*
87+
* See https://docs.python.org/3/library/socket.html
88+
*/
89+
API::Node socketInstance() { result = API::moduleImport("socket").getMember("socket").getReturn() }
90+
91+
from DataFlow::CallCfgNode bindCall, DataFlow::Node addressArg, string hostname
3392
where
34-
call = socketBindCall() and
35-
call.getArg(0).pointsTo(args) and
36-
args.getItem(0) = getTextValue(address) and
37-
address = allInterfaces()
38-
select call.getNode(), "'" + address + "' binds a socket to all interfaces."
93+
bindCall = socketInstance().getMember("bind").getACall() and
94+
addressArg = bindCall.getArg(0) and
95+
addressArg = vulnerableAddressTuple(hostname)
96+
select bindCall.asExpr(), "'" + hostname + "' binds a socket to all interfaces."
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @name Binding a socket to all network interfaces
3+
* @description Binding a socket to all interfaces opens it up to traffic from any IPv4 address
4+
* and is therefore associated with security risks.
5+
* @kind problem
6+
*/
7+
8+
import python
9+
10+
Value aSocket() { result.getClass() = Value::named("socket.socket") }
11+
12+
CallNode socketBindCall() {
13+
result = aSocket().attr("bind").(CallableValue).getACall() and major_version() = 3
14+
or
15+
result.getFunction().(AttrNode).getObject("bind").pointsTo(aSocket()) and
16+
major_version() = 2
17+
}
18+
19+
string allInterfaces() { result = "0.0.0.0" or result = "" }
20+
21+
Value getTextValue(string address) {
22+
result = Value::forUnicode(address) and major_version() = 3
23+
or
24+
result = Value::forString(address) and major_version() = 2
25+
}
26+
27+
from CallNode call, TupleValue args, string address
28+
where
29+
call = socketBindCall() and
30+
call.getArg(0).pointsTo(args) and
31+
args.getItem(0) = getTextValue(address) and
32+
address = allInterfaces()
33+
select call.getNode(), "'" + address + "' binds a socket to all interfaces."
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
| BindToAllInterfaces_test.py:5:1:5:26 | Attribute() | '0.0.0.0' binds a socket to all interfaces. |
22
| BindToAllInterfaces_test.py:9:1:9:18 | Attribute() | '' binds a socket to all interfaces. |
33
| BindToAllInterfaces_test.py:17:1:17:26 | Attribute() | '0.0.0.0' binds a socket to all interfaces. |
4+
| BindToAllInterfaces_test.py:21:1:21:11 | Attribute() | '0.0.0.0' binds a socket to all interfaces. |
5+
| BindToAllInterfaces_test.py:26:1:26:20 | Attribute() | '::' binds a socket to all interfaces. |

python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,12 @@
1515
# binds to all interfaces, insecure
1616
ALL_LOCALS = "0.0.0.0"
1717
s.bind((ALL_LOCALS, 9090))
18+
19+
# binds to all interfaces, insecure
20+
tup = (ALL_LOCALS, 8080)
21+
s.bind(tup)
22+
23+
24+
# IPv6
25+
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
26+
s.bind(("::", 8080)) # NOT OK

0 commit comments

Comments
 (0)