Skip to content

Commit

Permalink
[flake8-bandit] Implement S502 SslInsecureVersion rule (#9390)
Browse files Browse the repository at this point in the history
## Summary

Adds S502 rule for the
[flake8-bandit](https://github.com/tylerwince/flake8-bandit) plugin
port.

Checks for calls to any function with keywords arguments `ssl_version`
or `method` or for kwargs `method` in calls to `OpenSSL.SSL.Context` and
`ssl_version` in calls to `ssl.wrap_socket` which have an insecure
ssl_version valu. See also
https://bandit.readthedocs.io/en/latest/_modules/bandit/plugins/insecure_ssl_tls.html#ssl_with_bad_version

## Test Plan

Fixture added

## Issue Link

Refers: #1646
  • Loading branch information
qdegraaf committed Jan 5, 2024
1 parent 60ba7a7 commit 6dfc1cc
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 0 deletions.
16 changes: 16 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/flake8_bandit/S502.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ssl
from ssl import wrap_socket
from OpenSSL import SSL
from OpenSSL.SSL import Context

wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
SSL.Context(method=SSL.SSLv2_METHOD) # S502
SSL.Context(method=SSL.SSLv23_METHOD) # S502
Context(method=SSL.SSLv3_METHOD) # S502
Context(method=SSL.TLSv1_METHOD) # S502

wrap_socket(ssl_version=ssl.PROTOCOL_TLS_CLIENT) # OK
SSL.Context(method=SSL.TLS_SERVER_METHOD) # OK
func(ssl_version=ssl.PROTOCOL_TLSv1_2) # OK
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::SslWithNoVersion) {
flake8_bandit::rules::ssl_with_no_version(checker, call);
}
if checker.enabled(Rule::SslInsecureVersion) {
flake8_bandit::rules::ssl_insecure_version(checker, call);
}
}
Expr::Dict(dict) => {
if checker.any_enabled(&[
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bandit, "413") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPycryptoImport),
(Flake8Bandit, "415") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPyghmiImport),
(Flake8Bandit, "501") => (RuleGroup::Stable, rules::flake8_bandit::rules::RequestWithNoCertValidation),
(Flake8Bandit, "502") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslInsecureVersion),
(Flake8Bandit, "504") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslWithNoVersion),
(Flake8Bandit, "505") => (RuleGroup::Preview, rules::flake8_bandit::rules::WeakCryptographicKey),
(Flake8Bandit, "506") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnsafeYAMLLoad),
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/flake8_bandit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod tests {
#[test_case(Rule::SSHNoHostKeyVerification, Path::new("S507.py"))]
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))]
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))]
#[test_case(Rule::SslInsecureVersion, Path::new("S502.py"))]
#[test_case(Rule::SslWithNoVersion, Path::new("S504.py"))]
#[test_case(Rule::StartProcessWithAShell, Path::new("S605.py"))]
#[test_case(Rule::StartProcessWithNoShell, Path::new("S606.py"))]
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/flake8_bandit/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(crate) use shell_injection::*;
pub(crate) use snmp_insecure_version::*;
pub(crate) use snmp_weak_cryptography::*;
pub(crate) use ssh_no_host_key_verification::*;
pub(crate) use ssl_insecure_version::*;
pub(crate) use ssl_with_no_version::*;
pub(crate) use suspicious_function_call::*;
pub(crate) use suspicious_imports::*;
Expand Down Expand Up @@ -51,6 +52,7 @@ mod shell_injection;
mod snmp_insecure_version;
mod snmp_weak_cryptography;
mod ssh_no_host_key_verification;
mod ssl_insecure_version;
mod ssl_with_no_version;
mod suspicious_function_call;
mod suspicious_imports;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;

/// ## What it does
/// Checks for function calls with parameters that indicate the use of insecure
/// SSL and TLS protocol versions.
///
/// ## Why is this bad?
/// Several highly publicized exploitable flaws have been discovered in all
/// versions of SSL and early versions of TLS. The following versions are
/// considered insecure, and should be avoided:
/// - SSL v2
/// - SSL v3
/// - TLS v1
/// - TLS v1.1
///
/// This method supports detection on the Python's built-in `ssl` module and
/// the `pyOpenSSL` module.
///
/// ## Example
/// ```python
/// import ssl
///
/// ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1)
/// ```
///
/// Use instead:
/// ```python
/// import ssl
///
/// ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2)
/// ```
#[violation]
pub struct SslInsecureVersion {
protocol: String,
}

impl Violation for SslInsecureVersion {
#[derive_message_formats]
fn message(&self) -> String {
let SslInsecureVersion { protocol } = self;
format!("Call made with insecure SSL protocol: `{protocol}`")
}
}

/// S502
pub(crate) fn ssl_insecure_version(checker: &mut Checker, call: &ExprCall) {
let Some(keyword) = checker
.semantic()
.resolve_call_path(call.func.as_ref())
.and_then(|call_path| match call_path.as_slice() {
["ssl", "wrap_socket"] => Some("ssl_version"),
["OpenSSL", "SSL", "Context"] => Some("method"),
_ => None,
})
else {
return;
};

let Some(keyword) = call.arguments.find_keyword(keyword) else {
return;
};

match &keyword.value {
Expr::Name(ast::ExprName { id, .. }) => {
if is_insecure_protocol(id) {
checker.diagnostics.push(Diagnostic::new(
SslInsecureVersion {
protocol: id.to_string(),
},
keyword.range(),
));
}
}
Expr::Attribute(ast::ExprAttribute { attr, .. }) => {
if is_insecure_protocol(attr) {
checker.diagnostics.push(Diagnostic::new(
SslInsecureVersion {
protocol: attr.to_string(),
},
keyword.range(),
));
}
}
_ => {}
}
}

/// Returns `true` if the given protocol name is insecure.
fn is_insecure_protocol(name: &str) -> bool {
matches!(
name,
"PROTOCOL_SSLv2"
| "PROTOCOL_SSLv3"
| "PROTOCOL_TLSv1"
| "PROTOCOL_TLSv1_1"
| "SSLv2_METHOD"
| "SSLv23_METHOD"
| "SSLv3_METHOD"
| "TLSv1_METHOD"
| "TLSv1_1_METHOD"
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S502.py:6:13: S502 Call made with insecure SSL protocol: `PROTOCOL_SSLv3`
|
4 | from OpenSSL.SSL import Context
5 |
6 | wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S502
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
|

S502.py:7:17: S502 Call made with insecure SSL protocol: `PROTOCOL_TLSv1`
|
6 | wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S502
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
|

S502.py:8:17: S502 Call made with insecure SSL protocol: `PROTOCOL_SSLv2`
|
6 | wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S502
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
|

S502.py:9:13: S502 Call made with insecure SSL protocol: `SSLv2_METHOD`
|
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^ S502
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
11 | Context(method=SSL.SSLv3_METHOD) # S502
|

S502.py:10:13: S502 Call made with insecure SSL protocol: `SSLv23_METHOD`
|
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^^ S502
11 | Context(method=SSL.SSLv3_METHOD) # S502
12 | Context(method=SSL.TLSv1_METHOD) # S502
|

S502.py:11:9: S502 Call made with insecure SSL protocol: `SSLv3_METHOD`
|
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
11 | Context(method=SSL.SSLv3_METHOD) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^ S502
12 | Context(method=SSL.TLSv1_METHOD) # S502
|

S502.py:12:9: S502 Call made with insecure SSL protocol: `TLSv1_METHOD`
|
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
11 | Context(method=SSL.SSLv3_METHOD) # S502
12 | Context(method=SSL.TLSv1_METHOD) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^ S502
13 |
14 | wrap_socket(ssl_version=ssl.PROTOCOL_TLS_CLIENT) # OK
|


1 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6dfc1cc

Please sign in to comment.