diff --git a/its/ruling/src/test/resources/expected/python-S4423.json b/its/ruling/src/test/resources/expected/python-S4423.json new file mode 100644 index 0000000000..a941180048 --- /dev/null +++ b/its/ruling/src/test/resources/expected/python-S4423.json @@ -0,0 +1,78 @@ +{ +'project:buildbot-0.8.6p1/buildbot/status/mail.py':[ +40, +749, +], +'project:docker-compose-1.24.1/tests/unit/cli/docker_client_test.py':[ +174, +213, +], +'project:tornado-2.3/tornado/simple_httpclient.py':[ +210, +], +'project:tornado-2.3/tornado/test/httpserver_test.py':[ +110, +115, +120, +125, +], +'project:twisted-12.1.0/doc/core/examples/echoserv_ssl.py':[ +15, +], +'project:twisted-12.1.0/doc/core/howto/tutorial/listings/finger/finger/finger.py':[ +324, +], +'project:twisted-12.1.0/doc/core/howto/tutorial/listings/finger/finger22.py':[ +315, +], +'project:twisted-12.1.0/doc/mail/examples/smtpclient_tls.py':[ +9, +40, +], +'project:twisted-12.1.0/twisted/internet/_sslverify.py':[ +600, +], +'project:twisted-12.1.0/twisted/internet/endpoints.py':[ +1091, +], +'project:twisted-12.1.0/twisted/internet/ssl.py':[ +54, +106, +], +'project:twisted-12.1.0/twisted/mail/imap4.py':[ +2739, +], +'project:twisted-12.1.0/twisted/mail/pop3client.py':[ +428, +], +'project:twisted-12.1.0/twisted/mail/protocols.py':[ +222, +], +'project:twisted-12.1.0/twisted/mail/smtp.py':[ +1802, +], +'project:twisted-12.1.0/twisted/mail/test/pop3testserver.py':[ +211, +], +'project:twisted-12.1.0/twisted/protocols/test/test_tls.py':[ +20, +83, +], +'project:twisted-12.1.0/twisted/protocols/tls.py':[ +40, +43, +], +'project:twisted-12.1.0/twisted/test/ssl_helpers.py':[ +14, +23, +], +'project:twisted-12.1.0/twisted/test/test_ssl.py':[ +273, +665, +713, +], +'project:twisted-12.1.0/twisted/test/test_sslverify.py':[ +264, +285, +], +} diff --git a/python-checks/src/main/java/org/sonar/python/checks/CheckList.java b/python-checks/src/main/java/org/sonar/python/checks/CheckList.java index 1a4f1fca87..65b1b26a19 100644 --- a/python-checks/src/main/java/org/sonar/python/checks/CheckList.java +++ b/python-checks/src/main/java/org/sonar/python/checks/CheckList.java @@ -105,6 +105,7 @@ public static Iterable getChecks() { UnusedLocalVariableCheck.class, UselessParenthesisAfterKeywordCheck.class, UselessParenthesisCheck.class, + WeakSSLProtocolCheck.class, XPathCheck.class ))); } diff --git a/python-checks/src/main/java/org/sonar/python/checks/WeakSSLProtocolCheck.java b/python-checks/src/main/java/org/sonar/python/checks/WeakSSLProtocolCheck.java new file mode 100644 index 0000000000..9344361f9c --- /dev/null +++ b/python-checks/src/main/java/org/sonar/python/checks/WeakSSLProtocolCheck.java @@ -0,0 +1,68 @@ +/* + * SonarQube Python Plugin + * Copyright (C) 2011-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.python.checks; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import javax.annotation.Nullable; +import org.sonar.check.Rule; +import org.sonar.python.PythonSubscriptionCheck; +import org.sonar.python.api.tree.PyNameTree; +import org.sonar.python.api.tree.Tree; +import org.sonar.python.semantic.Symbol; + +@Rule(key = "S4423") +public class WeakSSLProtocolCheck extends PythonSubscriptionCheck { + private static final List WEAK_PROTOCOL_CONSTANTS = Arrays.asList( + "ssl.PROTOCOL_SSLv2", + "ssl.PROTOCOL_SSLv3", + "ssl.PROTOCOL_SSLv23", + "ssl.PROTOCOL_TLS", + "ssl.PROTOCOL_TLSv1", + "ssl.PROTOCOL_TLSv1_1", + "OpenSSL.SSL.SSLv2_METHOD", + "OpenSSL.SSL.SSLv3_METHOD", + "OpenSSL.SSL.SSLv23_METHOD", + "OpenSSL.SSL.TLSv1_METHOD", + "OpenSSL.SSL.TLSv1_1_METHOD" + ); + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(Tree.Kind.NAME, ctx -> { + PyNameTree pyNameTree = (PyNameTree) ctx.syntaxNode(); + Symbol symbol = ctx.symbolTable().getSymbol(pyNameTree); + if (isWeakProtocol(pyNameTree, symbol)) { + ctx.addIssue(pyNameTree, "Change this code to use a stronger protocol."); + } + }); + } + + private static boolean isWeakProtocol(PyNameTree pyNameTree, @Nullable Symbol symbol) { + Predicate matchWeakProtocol; + if (symbol == null) { + matchWeakProtocol = s -> s.substring(s.lastIndexOf('.') + 1).equals(pyNameTree.name()); + } else { + matchWeakProtocol = symbol.qualifiedName()::equals; + } + return WEAK_PROTOCOL_CONSTANTS.stream().anyMatch(matchWeakProtocol); + } +} diff --git a/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S4423.html b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S4423.html new file mode 100644 index 0000000000..5d6e3752a4 --- /dev/null +++ b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S4423.html @@ -0,0 +1,44 @@ +

Older versions of SSL/TLS protocol like "SSLv3" have been proven to be insecure.

+

This rule raises an issue when an SSL/TLS context is created with an insecure protocol version, i.e. when one of the following constants is +detected in the code:

+
    +
  • OpenSSL.SSL.SSLv3_METHOD (Use instead OpenSSL.SSL.TLSv1_2_METHOD)
  • +
  • ssl.PROTOCOL_SSLv3 (Use instead ssl.PROTOCOL_TLSv1_2)
  • +
+

Protocol versions different from TLSv1.2 and TLSv1.3 are considered insecure.

+

Noncompliant Code Example

+
+from OpenSSL import SSL
+
+SSL.Context(SSL.SSLv3_METHOD)  # Noncompliant
+
+
+import ssl
+
+ssl.SSLContext(ssl.PROTOCOL_SSLv3) # Noncompliant
+
+

Compliant Solution

+
+from OpenSSL import SSL
+
+SSL.Context(SSL.TLSv1_2_METHOD)  # Compliant
+
+
+import ssl
+
+ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) # Compliant
+
+

See

+ + diff --git a/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S4423.json b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S4423.json new file mode 100644 index 0000000000..7cda4afe6c --- /dev/null +++ b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S4423.json @@ -0,0 +1,34 @@ +{ + "title": "Weak SSL\/TLS protocols should not be used", + "type": "VULNERABILITY", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "2min" + }, + "tags": [ + "cwe", + "owasp-a6", + "sans-top25-porous", + "owasp-a3" + ], + "standards": [ + "CWE", + "OWASP Top 10", + "SANS Top 25" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-4423", + "sqKey": "S4423", + "scope": "Main", + "securityStandards": { + "CWE": [ + 327, + 326 + ], + "OWASP": [ + "A3", + "A6" + ] + } +} diff --git a/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json index 1fda2ecb09..45a3ec89b8 100644 --- a/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json +++ b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json @@ -35,6 +35,7 @@ "S2734", "S2772", "S3776", + "S4423", "S4507", "S4721", "S4784", diff --git a/python-checks/src/test/java/org/sonar/python/checks/WeakSSLProtocolCheckTest.java b/python-checks/src/test/java/org/sonar/python/checks/WeakSSLProtocolCheckTest.java new file mode 100644 index 0000000000..c19576cfc2 --- /dev/null +++ b/python-checks/src/test/java/org/sonar/python/checks/WeakSSLProtocolCheckTest.java @@ -0,0 +1,32 @@ +/* + * SonarQube Python Plugin + * Copyright (C) 2011-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.python.checks; + +import org.junit.Test; +import org.sonar.python.checks.utils.PythonCheckVerifier; + +public class WeakSSLProtocolCheckTest { + + @Test + public void test() { + PythonCheckVerifier.verify("src/test/resources/checks/weakSSLProtocol.py", new WeakSSLProtocolCheck()); + } + +} diff --git a/python-checks/src/test/resources/checks/weakSSLProtocol.py b/python-checks/src/test/resources/checks/weakSSLProtocol.py new file mode 100644 index 0000000000..1d3b3b615f --- /dev/null +++ b/python-checks/src/test/resources/checks/weakSSLProtocol.py @@ -0,0 +1,56 @@ +from OpenSSL import SSL +import ssl +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.poolmanager import PoolManager + +SSL.Context(SSL.SSLv2_METHOD) # Noncompliant {{Change this code to use a stronger protocol.}} +# ^^^^^^^^^^^^ +# Keyword argument +SSL.Context(method=SSL.SSLv2_METHOD) # Noncompliant +SSL.Context(SSL.TLSv1_2_METHOD) # Compliant +SSL.Context(method=SSL.TLSv1_2_METHOD) # Compliant + +# ssl.SSLContext() +ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv2) # Noncompliant +ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_SSLv2) # Noncompliant + +# ssl.wrap_socket() +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +ssl.wrap_socket(s, ssl_version=ssl.PROTOCOL_SSLv2) # Noncompliant +# ^^^^^^^^^^^^^^ + +# ssl.SSLContext() +ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) # Compliant +ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2) # Compliant + + +# ssl.wrap_socket() +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +ssl.wrap_socket(s, ssl_version=ssl.PROTOCOL_TLSv1_2) # Compliant + + +class Ssl3Adapter(HTTPAdapter): + """"Transport adapter that forces SSLv3""" + + def init_poolmanager(self, *pool_args, **pool_kwargs): + + self.poolmanager = PoolManager( + *pool_args, + ssl_version=ssl.PROTOCOL_SSLv3, # Noncompliant + **pool_kwargs) + +class Tls12Adapter(HTTPAdapter): + """"Transport adapter that forces TLSv1.2""" + + def init_poolmanager(self, *pool_args, **pool_kwargs): + self.poolmanager = PoolManager( + *pool_args, + ssl_version=ssl.PROTOCOL_TLSv1_2, + **pool_kwargs) + + +class unrelated(): + someClass.S = toto + PROTOCOL_SSLv2 = "someconstant" # Noncompliant FP due to lack of semantic + def met(): + foo(PROTOCOL_SSLv2) # compliant, symbol does not match qualified name diff --git a/sonar-python-plugin/src/test/java/org/sonar/plugins/python/PythonRuleRepositoryTest.java b/sonar-python-plugin/src/test/java/org/sonar/plugins/python/PythonRuleRepositoryTest.java index 6924b94b2f..4d0127228b 100644 --- a/sonar-python-plugin/src/test/java/org/sonar/plugins/python/PythonRuleRepositoryTest.java +++ b/sonar-python-plugin/src/test/java/org/sonar/plugins/python/PythonRuleRepositoryTest.java @@ -40,7 +40,7 @@ public void createRulesTest() { List rules = repository.rules(); assertThat(rules).isNotNull(); - assertThat(rules).hasSize(64); + assertThat(rules).hasSize(65); RulesDefinition.Rule s1578 = repository.rule("S1578"); assertThat(s1578).isNotNull();