Skip to content

Commit 37db21d

Browse files
authored
Merge pull request github#5284 from yoff/python-port-insecure-protocol
Python: port py/insecure-protocol
2 parents 0e7eeb3 + 6408ee2 commit 37db21d

File tree

12 files changed

+904
-118
lines changed

12 files changed

+904
-118
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Ported use of insecure SSL/TLS version (`py/insecure-protocol`) query to use new data-flow library. This might result in different results, but overall a more robust and accurate analysis.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
private import python
2+
private import semmle.python.dataflow.new.DataFlow
3+
import TlsLibraryModel
4+
5+
/**
6+
* Configuration to determine the state of a context being used to create
7+
* a connection. There is one configuration for each pair of `TlsLibrary` and `ProtocolVersion`,
8+
* such that a single configuration only tracks contexts where a specific `ProtocolVersion` is allowed.
9+
*
10+
* The state is in terms of whether a specific protocol is allowed. This is
11+
* either true or false when the context is created and can then be modified
12+
* later by either restricting or unrestricting the protocol (see the predicates
13+
* `isRestriction` and `isUnrestriction`).
14+
*
15+
* Since we are interested in the final state, we want the flow to start from
16+
* the last unrestriction, so we disallow flow into unrestrictions. We also
17+
* model the creation as an unrestriction of everything it allows, to account
18+
* for the common case where the creation plays the role of "last unrestriction".
19+
*
20+
* Since we really want "the last unrestriction, not nullified by a restriction",
21+
* we also disallow flow into restrictions.
22+
*/
23+
class InsecureContextConfiguration extends DataFlow::Configuration {
24+
TlsLibrary library;
25+
ProtocolVersion tracked_version;
26+
27+
InsecureContextConfiguration() {
28+
this = library + "Allows" + tracked_version and
29+
tracked_version.isInsecure()
30+
}
31+
32+
ProtocolVersion getTrackedVersion() { result = tracked_version }
33+
34+
override predicate isSource(DataFlow::Node source) { this.isUnrestriction(source) }
35+
36+
override predicate isSink(DataFlow::Node sink) {
37+
sink = library.connection_creation().getContext()
38+
}
39+
40+
override predicate isBarrierIn(DataFlow::Node node) {
41+
this.isRestriction(node)
42+
or
43+
this.isUnrestriction(node)
44+
}
45+
46+
private predicate isRestriction(DataFlow::Node node) {
47+
exists(ProtocolRestriction r |
48+
r = library.protocol_restriction() and
49+
r.getRestriction() = tracked_version
50+
|
51+
node = r.getContext()
52+
)
53+
}
54+
55+
private predicate isUnrestriction(DataFlow::Node node) {
56+
exists(ProtocolUnrestriction pu |
57+
pu = library.protocol_unrestriction() and
58+
pu.getUnrestriction() = tracked_version
59+
|
60+
node = pu.getContext()
61+
)
62+
}
63+
}
64+
65+
/**
66+
* Holds if `conectionCreation` marks the creation of a connetion based on the contex
67+
* found at `contextOrigin` and allowing `insecure_version`.
68+
*
69+
* `specific` is true iff the context is configured for a specific protocol version (`ssl.PROTOCOL_TLSv1_2`) rather
70+
* than for a family of protocols (`ssl.PROTOCOL_TLS`).
71+
*/
72+
predicate unsafe_connection_creation_with_context(
73+
DataFlow::Node connectionCreation, ProtocolVersion insecure_version, DataFlow::Node contextOrigin,
74+
boolean specific
75+
) {
76+
// Connection created from a context allowing `insecure_version`.
77+
exists(InsecureContextConfiguration c | c.hasFlow(contextOrigin, connectionCreation) |
78+
insecure_version = c.getTrackedVersion() and
79+
specific = false
80+
)
81+
or
82+
// Connection created from a context specifying `insecure_version`.
83+
exists(TlsLibrary l |
84+
connectionCreation = l.insecure_connection_creation(insecure_version) and
85+
contextOrigin = connectionCreation and
86+
specific = true
87+
)
88+
}
89+
90+
/**
91+
* Holds if `conectionCreation` marks the creation of a connetion witout reference to a context
92+
* and allowing `insecure_version`.
93+
*/
94+
predicate unsafe_connection_creation_without_context(
95+
DataFlow::CallCfgNode connectionCreation, string insecure_version
96+
) {
97+
exists(TlsLibrary l | connectionCreation = l.insecure_connection_creation(insecure_version))
98+
}
99+
100+
/** Holds if `contextCreation` is creating a context tied to a specific insecure version. */
101+
predicate unsafe_context_creation(DataFlow::CallCfgNode contextCreation, string insecure_version) {
102+
exists(TlsLibrary l | contextCreation = l.insecure_context_creation(insecure_version))
103+
}

python/ql/src/Security/CWE-327/InsecureProtocol.qhelp

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
<p>
1515
Ensure that a modern, strong protocol is used. All versions of SSL,
16-
and TLS 1.0 are known to be vulnerable to attacks. Using TLS 1.1 or
17-
above is strongly recommended.
16+
and TLS versions 1.0 and 1.1 are known to be vulnerable to attacks.
17+
Using TLS 1.2 or above is strongly recommended.
1818
</p>
1919

2020
</recommendation>
@@ -30,20 +30,35 @@
3030

3131
<p>
3232
All cases should be updated to use a secure protocol, such as
33-
<code>PROTOCOL_TLSv1_1</code>.
33+
<code>PROTOCOL_TLSv1_2</code>.
3434
</p>
3535
<p>
3636
Note that <code>ssl.wrap_socket</code> has been deprecated in
37-
Python 3.7. A preferred alternative is to use
38-
<code>ssl.SSLContext</code>, which is supported in Python 2.7.9 and
39-
3.2 and later versions.
37+
Python 3.7. The recommended alternatives are:
4038
</p>
39+
<ul>
40+
<li><code>ssl.SSLContext</code> - supported in Python 2.7.9,
41+
3.2, and later versions</li>
42+
<li><code>ssl.create_default_context</code> - a convenience function,
43+
supported in Python 3.4 and later versions.</li>
44+
</ul>
45+
<p>
46+
Even when you use these alternatives, you should
47+
ensure that a safe protocol is used. The following code illustrates
48+
how to use flags (available since Python 3.2) or the `minimum_version`
49+
field (favored since Python 3.7) to restrict the protocols accepted when
50+
creating a connection.
51+
</p>
52+
53+
<sample src="examples/secure_default_protocol.py" />
4154
</example>
4255

4356
<references>
4457
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Transport_Layer_Security"> Transport Layer Security</a>.</li>
4558
<li>Python 3 documentation: <a href="https://docs.python.org/3/library/ssl.html#ssl.SSLContext"> class ssl.SSLContext</a>.</li>
4659
<li>Python 3 documentation: <a href="https://docs.python.org/3/library/ssl.html#ssl.wrap_socket"> ssl.wrap_socket</a>.</li>
60+
<li>Python 3 documentation: <a href="https://docs.python.org/3/library/ssl.html#functions-constants-and-exceptions"> notes on context creation</a>.</li>
61+
<li>Python 3 documentation: <a href="https://docs.python.org/3/library/ssl.html#ssl-security"> notes on security considerations</a>.</li>
4762
<li>pyOpenSSL documentation: <a href="https://pyopenssl.org/en/stable/api/ssl.html"> An interface to the SSL-specific parts of OpenSSL</a>.</li>
4863
</references>
4964

python/ql/src/Security/CWE-327/InsecureProtocol.ql

Lines changed: 60 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -10,86 +10,77 @@
1010
*/
1111

1212
import python
13+
import semmle.python.dataflow.new.DataFlow
14+
import FluentApiModel
1315

14-
private ModuleValue the_ssl_module() { result = Module::named("ssl") }
15-
16-
FunctionValue ssl_wrap_socket() { result = the_ssl_module().attr("wrap_socket") }
17-
18-
ClassValue ssl_Context_class() { result = the_ssl_module().attr("SSLContext") }
16+
// Helper for pretty printer `configName`.
17+
// This is a consequence of missing pretty priting.
18+
// We do not want to evaluate our bespoke pretty printer
19+
// for all `DataFlow::Node`s so we define a sub class of interesting ones.
20+
class ProtocolConfiguration extends DataFlow::Node {
21+
ProtocolConfiguration() {
22+
unsafe_connection_creation_with_context(_, _, this, _)
23+
or
24+
unsafe_connection_creation_without_context(this, _)
25+
or
26+
unsafe_context_creation(this, _)
27+
}
1928

20-
private ModuleValue the_pyOpenSSL_module() { result = Value::named("pyOpenSSL.SSL") }
29+
AstNode getNode() { result = this.asCfgNode().(CallNode).getFunction().getNode() }
30+
}
2131

22-
ClassValue the_pyOpenSSL_Context_class() { result = Value::named("pyOpenSSL.SSL.Context") }
32+
// Helper for pretty printer `callName`.
33+
// This is a consequence of missing pretty priting.
34+
// We do not want to evaluate our bespoke pretty printer
35+
// for all `AstNode`s so we define a sub class of interesting ones.
36+
//
37+
// Note that AstNode is abstract and AstNode_ is a library class, so
38+
// we have to extend @py_ast_node.
39+
class Nameable extends @py_ast_node {
40+
Nameable() {
41+
this = any(ProtocolConfiguration pc).getNode()
42+
or
43+
exists(Nameable attr | this = attr.(Attribute).getObject())
44+
}
2345

24-
string insecure_version_name() {
25-
// For `pyOpenSSL.SSL`
26-
result = "SSLv2_METHOD" or
27-
result = "SSLv23_METHOD" or
28-
result = "SSLv3_METHOD" or
29-
result = "TLSv1_METHOD" or
30-
// For the `ssl` module
31-
result = "PROTOCOL_SSLv2" or
32-
result = "PROTOCOL_SSLv3" or
33-
result = "PROTOCOL_SSLv23" or
34-
result = "PROTOCOL_TLS" or
35-
result = "PROTOCOL_TLSv1"
46+
string toString() { result = "AstNode" }
3647
}
3748

38-
/*
39-
* A syntactic check for cases where points-to analysis cannot infer the presence of
40-
* a protocol constant, e.g. if it has been removed in later versions of the `ssl`
41-
* library.
42-
*/
43-
44-
bindingset[named_argument]
45-
predicate probable_insecure_ssl_constant(
46-
CallNode call, string insecure_version, string named_argument
47-
) {
48-
exists(ControlFlowNode arg |
49-
arg = call.getArgByName(named_argument) or
50-
arg = call.getArg(0)
51-
|
52-
arg.(AttrNode).getObject(insecure_version).pointsTo(the_ssl_module())
53-
or
54-
arg.(NameNode).getId() = insecure_version and
55-
exists(Import imp |
56-
imp.getAnImportedModuleName() = "ssl" and
57-
imp.getAName().getAsname().(Name).getId() = insecure_version
58-
)
59-
)
49+
string callName(Nameable call) {
50+
result = call.(Name).getId()
51+
or
52+
exists(Attribute a | a = call | result = callName(a.getObject()) + "." + a.getName())
6053
}
6154

62-
predicate unsafe_ssl_wrap_socket_call(
63-
CallNode call, string method_name, string insecure_version, string named_argument
64-
) {
65-
(
66-
call = ssl_wrap_socket().getACall() and
67-
method_name = "deprecated method ssl.wrap_socket" and
68-
named_argument = "ssl_version"
69-
or
70-
call = ssl_Context_class().getACall() and
71-
named_argument = "protocol" and
72-
method_name = "ssl.SSLContext"
73-
) and
74-
insecure_version = insecure_version_name() and
75-
(
76-
call.getArgByName(named_argument).pointsTo(the_ssl_module().attr(insecure_version))
77-
or
78-
probable_insecure_ssl_constant(call, insecure_version, named_argument)
79-
)
55+
string configName(ProtocolConfiguration protocolConfiguration) {
56+
result =
57+
"call to " + callName(protocolConfiguration.asCfgNode().(CallNode).getFunction().getNode())
58+
or
59+
not protocolConfiguration.asCfgNode() instanceof CallNode and
60+
not protocolConfiguration instanceof ContextCreation and
61+
result = "context modification"
8062
}
8163

82-
predicate unsafe_pyOpenSSL_Context_call(CallNode call, string insecure_version) {
83-
call = the_pyOpenSSL_Context_class().getACall() and
84-
insecure_version = insecure_version_name() and
85-
call.getArg(0).pointsTo(the_pyOpenSSL_module().attr(insecure_version))
64+
string verb(boolean specific) {
65+
specific = true and result = "specified"
66+
or
67+
specific = false and result = "allowed"
8668
}
8769

88-
from CallNode call, string method_name, string insecure_version
70+
from
71+
DataFlow::Node connectionCreation, string insecure_version, DataFlow::Node protocolConfiguration,
72+
boolean specific
8973
where
90-
unsafe_ssl_wrap_socket_call(call, method_name, insecure_version, _)
74+
unsafe_connection_creation_with_context(connectionCreation, insecure_version,
75+
protocolConfiguration, specific)
76+
or
77+
unsafe_connection_creation_without_context(connectionCreation, insecure_version) and
78+
protocolConfiguration = connectionCreation and
79+
specific = true
9180
or
92-
unsafe_pyOpenSSL_Context_call(call, insecure_version) and method_name = "pyOpenSSL.SSL.Context"
93-
select call,
94-
"Insecure SSL/TLS protocol version " + insecure_version + " specified in call to " + method_name +
95-
"."
81+
unsafe_context_creation(protocolConfiguration, insecure_version) and
82+
connectionCreation = protocolConfiguration and
83+
specific = true
84+
select connectionCreation,
85+
"Insecure SSL/TLS protocol version " + insecure_version + " " + verb(specific) + " by $@ ",
86+
protocolConfiguration, configName(protocolConfiguration)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Provides modeling of SSL/TLS functionality of the `OpenSSL` module from the `pyOpenSSL` PyPI package.
3+
* See https://www.pyopenssl.org/en/stable/
4+
*/
5+
6+
private import python
7+
private import semmle.python.ApiGraphs
8+
import TlsLibraryModel
9+
10+
class PyOpenSSLContextCreation extends ContextCreation, DataFlow::CallCfgNode {
11+
PyOpenSSLContextCreation() {
12+
this = API::moduleImport("OpenSSL").getMember("SSL").getMember("Context").getACall()
13+
}
14+
15+
override string getProtocol() {
16+
exists(ControlFlowNode protocolArg, PyOpenSSL pyo |
17+
protocolArg in [node.getArg(0), node.getArgByName("method")]
18+
|
19+
protocolArg =
20+
[pyo.specific_version(result).getAUse(), pyo.unspecific_version(result).getAUse()]
21+
.asCfgNode()
22+
)
23+
}
24+
}
25+
26+
class ConnectionCall extends ConnectionCreation, DataFlow::CallCfgNode {
27+
ConnectionCall() {
28+
this = API::moduleImport("OpenSSL").getMember("SSL").getMember("Connection").getACall()
29+
}
30+
31+
override DataFlow::CfgNode getContext() {
32+
result.getNode() in [node.getArg(0), node.getArgByName("context")]
33+
}
34+
}
35+
36+
// This cannot be used to unrestrict,
37+
// see https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_options
38+
class SetOptionsCall extends ProtocolRestriction, DataFlow::CallCfgNode {
39+
SetOptionsCall() { node.getFunction().(AttrNode).getName() = "set_options" }
40+
41+
override DataFlow::CfgNode getContext() {
42+
result.getNode() = node.getFunction().(AttrNode).getObject()
43+
}
44+
45+
override ProtocolVersion getRestriction() {
46+
API::moduleImport("OpenSSL").getMember("SSL").getMember("OP_NO_" + result).getAUse().asCfgNode() in [
47+
node.getArg(0), node.getArgByName("options")
48+
]
49+
}
50+
}
51+
52+
class UnspecificPyOpenSSLContextCreation extends PyOpenSSLContextCreation, UnspecificContextCreation {
53+
UnspecificPyOpenSSLContextCreation() { library instanceof PyOpenSSL }
54+
}
55+
56+
class PyOpenSSL extends TlsLibrary {
57+
PyOpenSSL() { this = "pyOpenSSL" }
58+
59+
override string specific_version_name(ProtocolVersion version) { result = version + "_METHOD" }
60+
61+
override string unspecific_version_name(ProtocolFamily family) {
62+
// `"TLS_METHOD"` is not actually available in pyOpenSSL yet, but should be coming soon..
63+
result = family + "_METHOD"
64+
}
65+
66+
override API::Node version_constants() { result = API::moduleImport("OpenSSL").getMember("SSL") }
67+
68+
override ContextCreation default_context_creation() { none() }
69+
70+
override ContextCreation specific_context_creation() {
71+
result instanceof PyOpenSSLContextCreation
72+
}
73+
74+
override DataFlow::Node insecure_connection_creation(ProtocolVersion version) { none() }
75+
76+
override ConnectionCreation connection_creation() { result instanceof ConnectionCall }
77+
78+
override ProtocolRestriction protocol_restriction() { result instanceof SetOptionsCall }
79+
80+
override ProtocolUnrestriction protocol_unrestriction() {
81+
result instanceof UnspecificPyOpenSSLContextCreation
82+
}
83+
}

0 commit comments

Comments
 (0)