From 824d004a2700080b8fb4bb3d2e3a02f85fd8e30a Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 26 Mar 2026 14:40:54 +0000 Subject: [PATCH 1/8] Python: Convert BindToAllInterfaces test to inline expectations --- .../Security/CVE-2018-1281/BindToAllInterfaces.qlref | 3 ++- .../Security/CVE-2018-1281/BindToAllInterfaces_test.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.qlref b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.qlref index f06cc3d869dc..6396fd918634 100644 --- a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.qlref +++ b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.qlref @@ -1 +1,2 @@ -Security/CVE-2018-1281/BindToAllInterfaces.ql \ No newline at end of file +query: Security/CVE-2018-1281/BindToAllInterfaces.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py index bbab44d81033..93ed0364a295 100644 --- a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py +++ b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py @@ -2,11 +2,11 @@ # binds to all interfaces, insecure s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -s.bind(('0.0.0.0', 31137)) +s.bind(('0.0.0.0', 31137)) # $ Alert[py/bind-socket-all-network-interfaces] # binds to all interfaces, insecure s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -s.bind(('', 4040)) +s.bind(('', 4040)) # $ Alert[py/bind-socket-all-network-interfaces] # binds only to a dedicated interface, secure s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -14,13 +14,13 @@ # binds to all interfaces, insecure ALL_LOCALS = "0.0.0.0" -s.bind((ALL_LOCALS, 9090)) +s.bind((ALL_LOCALS, 9090)) # $ Alert[py/bind-socket-all-network-interfaces] # binds to all interfaces, insecure tup = (ALL_LOCALS, 8080) -s.bind(tup) +s.bind(tup) # $ Alert[py/bind-socket-all-network-interfaces] # IPv6 s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) -s.bind(("::", 8080)) # NOT OK +s.bind(("::", 8080)) # $ Alert[py/bind-socket-all-network-interfaces] From 1ecd9e83b8566c9028cec3d33097f157a2b3bde1 Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 26 Mar 2026 14:51:59 +0000 Subject: [PATCH 2/8] Python: Add test cases for BindToAllInterfaces FNs Adds test cases from github/codeql#21582 demonstrating false negatives: - Address stored in class attribute (`self.bind_addr`) - `os.environ.get` with insecure default value - `gevent.socket` (alternative socket module) --- .../CVE-2018-1281/BindToAllInterfaces_test.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py index 93ed0364a295..5a13aa9c4e3a 100644 --- a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py +++ b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py @@ -24,3 +24,35 @@ # IPv6 s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) s.bind(("::", 8080)) # $ Alert[py/bind-socket-all-network-interfaces] + + +# FN cases from https://github.com/github/codeql/issues/21582 + +# Address stored in a class attribute +class Server: + def __init__(self): + self.bind_addr = '0.0.0.0' + self.port = 31137 + + def start(self): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind((self.bind_addr, self.port)) # $ MISSING: Alert[py/bind-socket-all-network-interfaces] + +server = Server() +server.start() + +# os.environ.get with insecure default +import os +host = os.environ.get('APP_HOST', '0.0.0.0') +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.bind((host, 8080)) # $ MISSING: Alert[py/bind-socket-all-network-interfaces] + +# gevent.socket (alternative socket module) +from gevent import socket as gsocket +gs = gsocket.socket(gsocket.AF_INET, gsocket.SOCK_STREAM) +gs.bind(('0.0.0.0', 31137)) # $ MISSING: Alert[py/bind-socket-all-network-interfaces] + +# eventlet.green.socket (another alternative socket module) +from eventlet.green import socket as esocket +es = esocket.socket(esocket.AF_INET, esocket.SOCK_STREAM) +es.bind(('0.0.0.0', 31137)) # $ MISSING: Alert[py/bind-socket-all-network-interfaces] From c439fc5d4519e8688115bc297a818f3420279e3e Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 26 Mar 2026 14:53:18 +0000 Subject: [PATCH 3/8] Python: Replace type tracking with global data-flow This takes care of most of the false negatives from the preceding commit. Additionally, we add models for some known wrappers of `socket.socket` from the `gevent` and `eventlet` packages. --- .../python/frameworks/Eventlet.model.yml | 9 ++++ .../semmle/python/frameworks/Gevent.model.yml | 7 +++ .../semmle/python/frameworks/Stdlib.model.yml | 4 ++ .../CVE-2018-1281/BindToAllInterfaces.ql | 51 +++++-------------- .../BindToAllInterfaces.expected | 4 ++ .../CVE-2018-1281/BindToAllInterfaces_test.py | 8 +-- shared/mad/codeql/mad/ModelValidation.qll | 2 +- 7 files changed, 43 insertions(+), 42 deletions(-) create mode 100644 python/ql/lib/semmle/python/frameworks/Eventlet.model.yml create mode 100644 python/ql/lib/semmle/python/frameworks/Gevent.model.yml diff --git a/python/ql/lib/semmle/python/frameworks/Eventlet.model.yml b/python/ql/lib/semmle/python/frameworks/Eventlet.model.yml new file mode 100644 index 000000000000..f60b9218819d --- /dev/null +++ b/python/ql/lib/semmle/python/frameworks/Eventlet.model.yml @@ -0,0 +1,9 @@ +extensions: + - addsTo: + pack: codeql/python-all + extensible: typeModel + data: + # See https://eventlet.readthedocs.io/en/latest/patching.html + - ['socket.socket', 'eventlet', 'Member[green].Member[socket].Member[socket].ReturnValue'] + # eventlet also re-exports as eventlet.socket for convenience + - ['socket.socket', 'eventlet', 'Member[socket].Member[socket].ReturnValue'] diff --git a/python/ql/lib/semmle/python/frameworks/Gevent.model.yml b/python/ql/lib/semmle/python/frameworks/Gevent.model.yml new file mode 100644 index 000000000000..974ecedd0730 --- /dev/null +++ b/python/ql/lib/semmle/python/frameworks/Gevent.model.yml @@ -0,0 +1,7 @@ +extensions: + - addsTo: + pack: codeql/python-all + extensible: typeModel + data: + # See https://www.gevent.org/api/gevent.socket.html + - ['socket.socket', 'gevent', 'Member[socket].Member[socket].ReturnValue'] diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib.model.yml b/python/ql/lib/semmle/python/frameworks/Stdlib.model.yml index a01bf1b40ba6..5b50dff313e7 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib.model.yml +++ b/python/ql/lib/semmle/python/frameworks/Stdlib.model.yml @@ -27,6 +27,8 @@ extensions: extensible: sinkModel data: - ["zipfile.ZipFile","Member[extractall].Argument[0,path:]", "path-injection"] + # See https://docs.python.org/3/library/socket.html#socket.socket.bind + - ["socket.socket", "Member[bind].Argument[0,address:]", "bind-socket-all-interfaces"] - addsTo: pack: codeql/python-all @@ -184,6 +186,8 @@ extensions: pack: codeql/python-all extensible: typeModel data: + # See https://docs.python.org/3/library/socket.html#socket.socket + - ['socket.socket', 'socket', 'Member[socket].ReturnValue'] # See https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlparse - ["urllib.parse.ParseResult~Subclass", 'urllib', 'Member[parse].Member[urlparse]'] diff --git a/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql b/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql index 5e2e27b3bf40..39d0c6b6237d 100644 --- a/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql +++ b/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql @@ -14,7 +14,8 @@ import python import semmle.python.dataflow.new.DataFlow -import semmle.python.ApiGraphs +import semmle.python.dataflow.new.TaintTracking +private import semmle.python.frameworks.data.ModelsAsData /** Gets a hostname that can be used to bind to all interfaces. */ private string vulnerableHostname() { @@ -26,45 +27,21 @@ private string vulnerableHostname() { ] } -/** Gets a reference to a hostname that can be used to bind to all interfaces. */ -private DataFlow::TypeTrackingNode vulnerableHostnameRef(DataFlow::TypeTracker t, string hostname) { - t.start() and - exists(StringLiteral allInterfacesStringLiteral | hostname = vulnerableHostname() | - allInterfacesStringLiteral.getText() = hostname and - result.asExpr() = allInterfacesStringLiteral - ) - or - exists(DataFlow::TypeTracker t2 | result = vulnerableHostnameRef(t2, hostname).track(t2, t)) -} - -/** Gets a reference to a hostname that can be used to bind to all interfaces. */ -DataFlow::Node vulnerableHostnameRef(string hostname) { - vulnerableHostnameRef(DataFlow::TypeTracker::end(), hostname).flowsTo(result) -} +private module BindToAllInterfacesConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source.asExpr().(StringLiteral).getText() = vulnerableHostname() + } -/** Gets a reference to a tuple for which the first element is a hostname that can be used to bind to all interfaces. */ -private DataFlow::TypeTrackingNode vulnerableAddressTuple(DataFlow::TypeTracker t, string hostname) { - t.start() and - result.asExpr() = any(Tuple tup | tup.getElt(0) = vulnerableHostnameRef(hostname).asExpr()) - or - exists(DataFlow::TypeTracker t2 | result = vulnerableAddressTuple(t2, hostname).track(t2, t)) + predicate isSink(DataFlow::Node sink) { + ModelOutput::sinkNode(sink, "bind-socket-all-interfaces") + } } -/** Gets a reference to a tuple for which the first element is a hostname that can be used to bind to all interfaces. */ -DataFlow::Node vulnerableAddressTuple(string hostname) { - vulnerableAddressTuple(DataFlow::TypeTracker::end(), hostname).flowsTo(result) -} - -/** - * Gets an instance of `socket.socket` using _some_ address family. - * - * See https://docs.python.org/3/library/socket.html - */ -API::Node socketInstance() { result = API::moduleImport("socket").getMember("socket").getReturn() } +private module BindToAllInterfacesFlow = TaintTracking::Global; -from DataFlow::CallCfgNode bindCall, DataFlow::Node addressArg, string hostname +from DataFlow::Node source, DataFlow::Node sink, DataFlow::CallCfgNode bindCall, string hostname where - bindCall = socketInstance().getMember("bind").getACall() and - addressArg = bindCall.getArg(0) and - addressArg = vulnerableAddressTuple(hostname) + BindToAllInterfacesFlow::flow(source, sink) and + bindCall.getArg(0) = sink and + hostname = source.asExpr().(StringLiteral).getText() select bindCall.asExpr(), "'" + hostname + "' binds a socket to all interfaces." diff --git a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.expected b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.expected index 86c67af4eaef..d657c2f14db4 100644 --- a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.expected +++ b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.expected @@ -3,3 +3,7 @@ | BindToAllInterfaces_test.py:17:1:17:26 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | | BindToAllInterfaces_test.py:21:1:21:11 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | | BindToAllInterfaces_test.py:26:1:26:20 | Attribute() | '::' binds a socket to all interfaces. | +| BindToAllInterfaces_test.py:39:9:39:43 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | +| BindToAllInterfaces_test.py:48:1:48:20 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | +| BindToAllInterfaces_test.py:53:1:53:27 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | +| BindToAllInterfaces_test.py:58:1:58:27 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | diff --git a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py index 5a13aa9c4e3a..8d5d7998101d 100644 --- a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py +++ b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py @@ -36,7 +36,7 @@ def __init__(self): def start(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind((self.bind_addr, self.port)) # $ MISSING: Alert[py/bind-socket-all-network-interfaces] + s.bind((self.bind_addr, self.port)) # $ Alert[py/bind-socket-all-network-interfaces] server = Server() server.start() @@ -45,14 +45,14 @@ def start(self): import os host = os.environ.get('APP_HOST', '0.0.0.0') s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -s.bind((host, 8080)) # $ MISSING: Alert[py/bind-socket-all-network-interfaces] +s.bind((host, 8080)) # $ Alert[py/bind-socket-all-network-interfaces] # gevent.socket (alternative socket module) from gevent import socket as gsocket gs = gsocket.socket(gsocket.AF_INET, gsocket.SOCK_STREAM) -gs.bind(('0.0.0.0', 31137)) # $ MISSING: Alert[py/bind-socket-all-network-interfaces] +gs.bind(('0.0.0.0', 31137)) # $ Alert[py/bind-socket-all-network-interfaces] # eventlet.green.socket (another alternative socket module) from eventlet.green import socket as esocket es = esocket.socket(esocket.AF_INET, esocket.SOCK_STREAM) -es.bind(('0.0.0.0', 31137)) # $ MISSING: Alert[py/bind-socket-all-network-interfaces] +es.bind(('0.0.0.0', 31137)) # $ Alert[py/bind-socket-all-network-interfaces] diff --git a/shared/mad/codeql/mad/ModelValidation.qll b/shared/mad/codeql/mad/ModelValidation.qll index 042fb4200dd1..5eaa78626ab3 100644 --- a/shared/mad/codeql/mad/ModelValidation.qll +++ b/shared/mad/codeql/mad/ModelValidation.qll @@ -48,7 +48,7 @@ module KindValidation { // CPP-only currently "remote-sink", // Python-only currently, but may be shared in the future - "prompt-injection" + "bind-socket-all-interfaces", "prompt-injection" ] or this.matches([ From c0ce6699a535488181d122316c04609e8afd9735 Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 26 Mar 2026 15:10:59 +0000 Subject: [PATCH 4/8] Python: Add change note --- .../2026-03-26-improve-bind-all-interfaces-query.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 python/ql/src/change-notes/2026-03-26-improve-bind-all-interfaces-query.md diff --git a/python/ql/src/change-notes/2026-03-26-improve-bind-all-interfaces-query.md b/python/ql/src/change-notes/2026-03-26-improve-bind-all-interfaces-query.md new file mode 100644 index 000000000000..b4b5464b5037 --- /dev/null +++ b/python/ql/src/change-notes/2026-03-26-improve-bind-all-interfaces-query.md @@ -0,0 +1,5 @@ +--- +category: minorAnalysis +--- + +- The `py/bind-socket-all-network-interfaces` query now uses the global data-flow library, leading to better precision and more results. Also, wrappers of `socket.socket` in the `eventlet` and `gevent` libraries are now also recognised as socket binding operations. From c9832c330af360b4204971cb1c250b7633da0c1d Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 26 Mar 2026 20:13:55 +0000 Subject: [PATCH 5/8] Python: Convert BindToAllInterfaces to path-problem Now that we're using global data-flow, we might as well make use of the fact that we know where the source is. --- .../CVE-2018-1281/BindToAllInterfaces.ql | 16 +++-- .../BindToAllInterfaces.expected | 72 ++++++++++++++++--- .../CVE-2018-1281/BindToAllInterfaces_test.py | 6 +- 3 files changed, 75 insertions(+), 19 deletions(-) diff --git a/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql b/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql index 39d0c6b6237d..2b62b184fd4e 100644 --- a/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql +++ b/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql @@ -2,7 +2,7 @@ * @name Binding a socket to all network interfaces * @description Binding a socket to all interfaces opens it up to traffic from any IPv4 address * and is therefore associated with security risks. - * @kind problem + * @kind path-problem * @tags security * external/cwe/cwe-200 * @problem.severity error @@ -16,6 +16,7 @@ import python import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TaintTracking private import semmle.python.frameworks.data.ModelsAsData +import BindToAllInterfacesFlow::PathGraph /** Gets a hostname that can be used to bind to all interfaces. */ private string vulnerableHostname() { @@ -39,9 +40,10 @@ private module BindToAllInterfacesConfig implements DataFlow::ConfigSig { private module BindToAllInterfacesFlow = TaintTracking::Global; -from DataFlow::Node source, DataFlow::Node sink, DataFlow::CallCfgNode bindCall, string hostname -where - BindToAllInterfacesFlow::flow(source, sink) and - bindCall.getArg(0) = sink and - hostname = source.asExpr().(StringLiteral).getText() -select bindCall.asExpr(), "'" + hostname + "' binds a socket to all interfaces." +private import BindToAllInterfacesFlow + +from PathNode source, PathNode sink +where flowPath(source, sink) +select sink.getNode(), source, sink, + "Binding a socket to all interfaces (using $@) is a security risk.", source.getNode(), + "'" + source.getNode().asExpr().(StringLiteral).getText() + "'" diff --git a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.expected b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.expected index d657c2f14db4..0b96b2df6508 100644 --- a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.expected +++ b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces.expected @@ -1,9 +1,63 @@ -| BindToAllInterfaces_test.py:5:1:5:26 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | -| BindToAllInterfaces_test.py:9:1:9:18 | Attribute() | '' binds a socket to all interfaces. | -| BindToAllInterfaces_test.py:17:1:17:26 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | -| BindToAllInterfaces_test.py:21:1:21:11 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | -| BindToAllInterfaces_test.py:26:1:26:20 | Attribute() | '::' binds a socket to all interfaces. | -| BindToAllInterfaces_test.py:39:9:39:43 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | -| BindToAllInterfaces_test.py:48:1:48:20 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | -| BindToAllInterfaces_test.py:53:1:53:27 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | -| BindToAllInterfaces_test.py:58:1:58:27 | Attribute() | '0.0.0.0' binds a socket to all interfaces. | +#select +| BindToAllInterfaces_test.py:5:9:5:24 | ControlFlowNode for Tuple | BindToAllInterfaces_test.py:5:9:5:17 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:5:9:5:24 | ControlFlowNode for Tuple | Binding a socket to all interfaces (using $@) is a security risk. | BindToAllInterfaces_test.py:5:9:5:17 | ControlFlowNode for StringLiteral | '0.0.0.0' | +| BindToAllInterfaces_test.py:9:9:9:16 | ControlFlowNode for Tuple | BindToAllInterfaces_test.py:9:9:9:10 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:9:9:9:16 | ControlFlowNode for Tuple | Binding a socket to all interfaces (using $@) is a security risk. | BindToAllInterfaces_test.py:9:9:9:10 | ControlFlowNode for StringLiteral | '' | +| BindToAllInterfaces_test.py:17:9:17:24 | ControlFlowNode for Tuple | BindToAllInterfaces_test.py:16:14:16:22 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:17:9:17:24 | ControlFlowNode for Tuple | Binding a socket to all interfaces (using $@) is a security risk. | BindToAllInterfaces_test.py:16:14:16:22 | ControlFlowNode for StringLiteral | '0.0.0.0' | +| BindToAllInterfaces_test.py:21:8:21:10 | ControlFlowNode for tup | BindToAllInterfaces_test.py:16:14:16:22 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:21:8:21:10 | ControlFlowNode for tup | Binding a socket to all interfaces (using $@) is a security risk. | BindToAllInterfaces_test.py:16:14:16:22 | ControlFlowNode for StringLiteral | '0.0.0.0' | +| BindToAllInterfaces_test.py:26:9:26:18 | ControlFlowNode for Tuple | BindToAllInterfaces_test.py:26:9:26:12 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:26:9:26:18 | ControlFlowNode for Tuple | Binding a socket to all interfaces (using $@) is a security risk. | BindToAllInterfaces_test.py:26:9:26:12 | ControlFlowNode for StringLiteral | '::' | +| BindToAllInterfaces_test.py:39:17:39:41 | ControlFlowNode for Tuple | BindToAllInterfaces_test.py:34:26:34:34 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:39:17:39:41 | ControlFlowNode for Tuple | Binding a socket to all interfaces (using $@) is a security risk. | BindToAllInterfaces_test.py:34:26:34:34 | ControlFlowNode for StringLiteral | '0.0.0.0' | +| BindToAllInterfaces_test.py:48:9:48:18 | ControlFlowNode for Tuple | BindToAllInterfaces_test.py:46:35:46:43 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:48:9:48:18 | ControlFlowNode for Tuple | Binding a socket to all interfaces (using $@) is a security risk. | BindToAllInterfaces_test.py:46:35:46:43 | ControlFlowNode for StringLiteral | '0.0.0.0' | +| BindToAllInterfaces_test.py:53:10:53:25 | ControlFlowNode for Tuple | BindToAllInterfaces_test.py:53:10:53:18 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:53:10:53:25 | ControlFlowNode for Tuple | Binding a socket to all interfaces (using $@) is a security risk. | BindToAllInterfaces_test.py:53:10:53:18 | ControlFlowNode for StringLiteral | '0.0.0.0' | +| BindToAllInterfaces_test.py:58:10:58:25 | ControlFlowNode for Tuple | BindToAllInterfaces_test.py:58:10:58:18 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:58:10:58:25 | ControlFlowNode for Tuple | Binding a socket to all interfaces (using $@) is a security risk. | BindToAllInterfaces_test.py:58:10:58:18 | ControlFlowNode for StringLiteral | '0.0.0.0' | +edges +| BindToAllInterfaces_test.py:5:9:5:17 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:5:9:5:24 | ControlFlowNode for Tuple | provenance | Sink:MaD:63 | +| BindToAllInterfaces_test.py:9:9:9:10 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:9:9:9:16 | ControlFlowNode for Tuple | provenance | Sink:MaD:63 | +| BindToAllInterfaces_test.py:16:1:16:10 | ControlFlowNode for ALL_LOCALS | BindToAllInterfaces_test.py:17:9:17:24 | ControlFlowNode for Tuple | provenance | Sink:MaD:63 | +| BindToAllInterfaces_test.py:16:1:16:10 | ControlFlowNode for ALL_LOCALS | BindToAllInterfaces_test.py:20:1:20:3 | ControlFlowNode for tup | provenance | | +| BindToAllInterfaces_test.py:16:14:16:22 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:16:1:16:10 | ControlFlowNode for ALL_LOCALS | provenance | | +| BindToAllInterfaces_test.py:20:1:20:3 | ControlFlowNode for tup | BindToAllInterfaces_test.py:21:8:21:10 | ControlFlowNode for tup | provenance | Sink:MaD:63 | +| BindToAllInterfaces_test.py:26:9:26:12 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:26:9:26:18 | ControlFlowNode for Tuple | provenance | Sink:MaD:63 | +| BindToAllInterfaces_test.py:33:18:33:21 | ControlFlowNode for self [Return] [Attribute bind_addr] | BindToAllInterfaces_test.py:41:10:41:17 | ControlFlowNode for Server() [Attribute bind_addr] | provenance | | +| BindToAllInterfaces_test.py:34:9:34:12 | [post] ControlFlowNode for self [Attribute bind_addr] | BindToAllInterfaces_test.py:33:18:33:21 | ControlFlowNode for self [Return] [Attribute bind_addr] | provenance | | +| BindToAllInterfaces_test.py:34:26:34:34 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:34:9:34:12 | [post] ControlFlowNode for self [Attribute bind_addr] | provenance | | +| BindToAllInterfaces_test.py:37:15:37:18 | ControlFlowNode for self [Attribute bind_addr] | BindToAllInterfaces_test.py:39:17:39:20 | ControlFlowNode for self [Attribute bind_addr] | provenance | | +| BindToAllInterfaces_test.py:39:17:39:20 | ControlFlowNode for self [Attribute bind_addr] | BindToAllInterfaces_test.py:39:17:39:30 | ControlFlowNode for Attribute | provenance | | +| BindToAllInterfaces_test.py:39:17:39:30 | ControlFlowNode for Attribute | BindToAllInterfaces_test.py:39:17:39:41 | ControlFlowNode for Tuple | provenance | Sink:MaD:63 | +| BindToAllInterfaces_test.py:41:1:41:6 | ControlFlowNode for server [Attribute bind_addr] | BindToAllInterfaces_test.py:42:1:42:6 | ControlFlowNode for server [Attribute bind_addr] | provenance | | +| BindToAllInterfaces_test.py:41:10:41:17 | ControlFlowNode for Server() [Attribute bind_addr] | BindToAllInterfaces_test.py:41:1:41:6 | ControlFlowNode for server [Attribute bind_addr] | provenance | | +| BindToAllInterfaces_test.py:42:1:42:6 | ControlFlowNode for server [Attribute bind_addr] | BindToAllInterfaces_test.py:37:15:37:18 | ControlFlowNode for self [Attribute bind_addr] | provenance | | +| BindToAllInterfaces_test.py:46:1:46:4 | ControlFlowNode for host | BindToAllInterfaces_test.py:48:9:48:18 | ControlFlowNode for Tuple | provenance | Sink:MaD:63 | +| BindToAllInterfaces_test.py:46:8:46:44 | ControlFlowNode for Attribute() | BindToAllInterfaces_test.py:46:1:46:4 | ControlFlowNode for host | provenance | | +| BindToAllInterfaces_test.py:46:35:46:43 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:46:8:46:44 | ControlFlowNode for Attribute() | provenance | dict.get | +| BindToAllInterfaces_test.py:53:10:53:18 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:53:10:53:25 | ControlFlowNode for Tuple | provenance | Sink:MaD:63 | +| BindToAllInterfaces_test.py:58:10:58:18 | ControlFlowNode for StringLiteral | BindToAllInterfaces_test.py:58:10:58:25 | ControlFlowNode for Tuple | provenance | Sink:MaD:63 | +nodes +| BindToAllInterfaces_test.py:5:9:5:17 | ControlFlowNode for StringLiteral | semmle.label | ControlFlowNode for StringLiteral | +| BindToAllInterfaces_test.py:5:9:5:24 | ControlFlowNode for Tuple | semmle.label | ControlFlowNode for Tuple | +| BindToAllInterfaces_test.py:9:9:9:10 | ControlFlowNode for StringLiteral | semmle.label | ControlFlowNode for StringLiteral | +| BindToAllInterfaces_test.py:9:9:9:16 | ControlFlowNode for Tuple | semmle.label | ControlFlowNode for Tuple | +| BindToAllInterfaces_test.py:16:1:16:10 | ControlFlowNode for ALL_LOCALS | semmle.label | ControlFlowNode for ALL_LOCALS | +| BindToAllInterfaces_test.py:16:14:16:22 | ControlFlowNode for StringLiteral | semmle.label | ControlFlowNode for StringLiteral | +| BindToAllInterfaces_test.py:17:9:17:24 | ControlFlowNode for Tuple | semmle.label | ControlFlowNode for Tuple | +| BindToAllInterfaces_test.py:20:1:20:3 | ControlFlowNode for tup | semmle.label | ControlFlowNode for tup | +| BindToAllInterfaces_test.py:21:8:21:10 | ControlFlowNode for tup | semmle.label | ControlFlowNode for tup | +| BindToAllInterfaces_test.py:26:9:26:12 | ControlFlowNode for StringLiteral | semmle.label | ControlFlowNode for StringLiteral | +| BindToAllInterfaces_test.py:26:9:26:18 | ControlFlowNode for Tuple | semmle.label | ControlFlowNode for Tuple | +| BindToAllInterfaces_test.py:33:18:33:21 | ControlFlowNode for self [Return] [Attribute bind_addr] | semmle.label | ControlFlowNode for self [Return] [Attribute bind_addr] | +| BindToAllInterfaces_test.py:34:9:34:12 | [post] ControlFlowNode for self [Attribute bind_addr] | semmle.label | [post] ControlFlowNode for self [Attribute bind_addr] | +| BindToAllInterfaces_test.py:34:26:34:34 | ControlFlowNode for StringLiteral | semmle.label | ControlFlowNode for StringLiteral | +| BindToAllInterfaces_test.py:37:15:37:18 | ControlFlowNode for self [Attribute bind_addr] | semmle.label | ControlFlowNode for self [Attribute bind_addr] | +| BindToAllInterfaces_test.py:39:17:39:20 | ControlFlowNode for self [Attribute bind_addr] | semmle.label | ControlFlowNode for self [Attribute bind_addr] | +| BindToAllInterfaces_test.py:39:17:39:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| BindToAllInterfaces_test.py:39:17:39:41 | ControlFlowNode for Tuple | semmle.label | ControlFlowNode for Tuple | +| BindToAllInterfaces_test.py:41:1:41:6 | ControlFlowNode for server [Attribute bind_addr] | semmle.label | ControlFlowNode for server [Attribute bind_addr] | +| BindToAllInterfaces_test.py:41:10:41:17 | ControlFlowNode for Server() [Attribute bind_addr] | semmle.label | ControlFlowNode for Server() [Attribute bind_addr] | +| BindToAllInterfaces_test.py:42:1:42:6 | ControlFlowNode for server [Attribute bind_addr] | semmle.label | ControlFlowNode for server [Attribute bind_addr] | +| BindToAllInterfaces_test.py:46:1:46:4 | ControlFlowNode for host | semmle.label | ControlFlowNode for host | +| BindToAllInterfaces_test.py:46:8:46:44 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| BindToAllInterfaces_test.py:46:35:46:43 | ControlFlowNode for StringLiteral | semmle.label | ControlFlowNode for StringLiteral | +| BindToAllInterfaces_test.py:48:9:48:18 | ControlFlowNode for Tuple | semmle.label | ControlFlowNode for Tuple | +| BindToAllInterfaces_test.py:53:10:53:18 | ControlFlowNode for StringLiteral | semmle.label | ControlFlowNode for StringLiteral | +| BindToAllInterfaces_test.py:53:10:53:25 | ControlFlowNode for Tuple | semmle.label | ControlFlowNode for Tuple | +| BindToAllInterfaces_test.py:58:10:58:18 | ControlFlowNode for StringLiteral | semmle.label | ControlFlowNode for StringLiteral | +| BindToAllInterfaces_test.py:58:10:58:25 | ControlFlowNode for Tuple | semmle.label | ControlFlowNode for Tuple | +subpaths diff --git a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py index 8d5d7998101d..3c267ff2f296 100644 --- a/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py +++ b/python/ql/test/query-tests/Security/CVE-2018-1281/BindToAllInterfaces_test.py @@ -13,7 +13,7 @@ s.bind(('84.68.10.12', 8080)) # binds to all interfaces, insecure -ALL_LOCALS = "0.0.0.0" +ALL_LOCALS = "0.0.0.0" # $ Source s.bind((ALL_LOCALS, 9090)) # $ Alert[py/bind-socket-all-network-interfaces] # binds to all interfaces, insecure @@ -31,7 +31,7 @@ # Address stored in a class attribute class Server: def __init__(self): - self.bind_addr = '0.0.0.0' + self.bind_addr = '0.0.0.0' # $ Source self.port = 31137 def start(self): @@ -43,7 +43,7 @@ def start(self): # os.environ.get with insecure default import os -host = os.environ.get('APP_HOST', '0.0.0.0') +host = os.environ.get('APP_HOST', '0.0.0.0') # $ Source s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind((host, 8080)) # $ Alert[py/bind-socket-all-network-interfaces] From 4f74d421b9095e06a8642a5615c48f0a0094d7aa Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 27 Mar 2026 14:11:14 +0000 Subject: [PATCH 6/8] Python: Exclude `AF_UNIX` sockets from BindToAllInterfaces Looking at the results of the the previous DCA run, there was a bunch of false positives where `bind` was being used with a `AF_UNIX` socket (a filesystem path encoded as a string), not a `(host, port)` tuple. These results should be excluded from the query, as they are not vulnerable. Ideally, we would just add `.TupleElement[0]` to the MaD sink, except we don't actually support this in Python MaD... So, instead I opted for a more low-tech solution: check that the argument in question flows from a tuple in the local scope. This eliminates a bunch of false positives on `python/cpython` leaving behind four true positive results. --- .../Security/CVE-2018-1281/BindToAllInterfaces.ql | 14 +++++++++++++- .../CVE-2018-1281/BindToAllInterfaces.expected | 1 + .../CVE-2018-1281/BindToAllInterfaces_test.py | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql b/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql index 2b62b184fd4e..75c145ec0ace 100644 --- a/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql +++ b/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql @@ -42,8 +42,20 @@ private module BindToAllInterfacesFlow = TaintTracking::Global Date: Fri, 27 Mar 2026 22:44:39 +0000 Subject: [PATCH 7/8] Python: Move isNetworkBind check into isSink --- .../CVE-2018-1281/BindToAllInterfaces.ql | 20 ++++++------------- .../BindToAllInterfaces.expected | 1 - 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql b/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql index 75c145ec0ace..14c17edc3591 100644 --- a/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql +++ b/python/ql/src/Security/CVE-2018-1281/BindToAllInterfaces.ql @@ -34,7 +34,11 @@ private module BindToAllInterfacesConfig implements DataFlow::ConfigSig { } predicate isSink(DataFlow::Node sink) { - ModelOutput::sinkNode(sink, "bind-socket-all-interfaces") + ModelOutput::sinkNode(sink, "bind-socket-all-interfaces") and + // Network socket addresses are tuples like (host, port), so we require + // the bind() argument to originate from a tuple expression. This excludes + // AF_UNIX sockets, which pass a plain string path to bind(). + any(DataFlow::LocalSourceNode n | n.asExpr() instanceof Tuple).flowsTo(sink) } } @@ -42,20 +46,8 @@ private module BindToAllInterfacesFlow = TaintTracking::Global Date: Fri, 27 Mar 2026 23:46:50 +0100 Subject: [PATCH 8/8] Python: Update change note Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../2026-03-26-improve-bind-all-interfaces-query.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/change-notes/2026-03-26-improve-bind-all-interfaces-query.md b/python/ql/src/change-notes/2026-03-26-improve-bind-all-interfaces-query.md index b4b5464b5037..bc78b2b6f776 100644 --- a/python/ql/src/change-notes/2026-03-26-improve-bind-all-interfaces-query.md +++ b/python/ql/src/change-notes/2026-03-26-improve-bind-all-interfaces-query.md @@ -2,4 +2,4 @@ category: minorAnalysis --- -- The `py/bind-socket-all-network-interfaces` query now uses the global data-flow library, leading to better precision and more results. Also, wrappers of `socket.socket` in the `eventlet` and `gevent` libraries are now also recognised as socket binding operations. +- The `py/bind-socket-all-network-interfaces` query now uses the global data-flow library, leading to better precision and more results. Also, wrappers of `socket.socket` in the `eventlet` and `gevent` libraries are now also recognized as socket binding operations.