mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 12:29:28 +00:00
[flake8-bandit
] Implement S502
SslInsecureVersion
rule (#9390)
## 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: https://github.com/astral-sh/ruff/issues/1646
This commit is contained in:
parent
60ba7a7c0d
commit
6dfc1ccd6f
8 changed files with 203 additions and 0 deletions
16
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S502.py
vendored
Normal file
16
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S502.py
vendored
Normal file
|
@ -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
|
|
@ -968,6 +968,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::SslWithNoVersion) {
|
if checker.enabled(Rule::SslWithNoVersion) {
|
||||||
flake8_bandit::rules::ssl_with_no_version(checker, call);
|
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) => {
|
Expr::Dict(dict) => {
|
||||||
if checker.any_enabled(&[
|
if checker.any_enabled(&[
|
||||||
|
|
|
@ -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, "413") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPycryptoImport),
|
||||||
(Flake8Bandit, "415") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPyghmiImport),
|
(Flake8Bandit, "415") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPyghmiImport),
|
||||||
(Flake8Bandit, "501") => (RuleGroup::Stable, rules::flake8_bandit::rules::RequestWithNoCertValidation),
|
(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, "504") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslWithNoVersion),
|
||||||
(Flake8Bandit, "505") => (RuleGroup::Preview, rules::flake8_bandit::rules::WeakCryptographicKey),
|
(Flake8Bandit, "505") => (RuleGroup::Preview, rules::flake8_bandit::rules::WeakCryptographicKey),
|
||||||
(Flake8Bandit, "506") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnsafeYAMLLoad),
|
(Flake8Bandit, "506") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnsafeYAMLLoad),
|
||||||
|
|
|
@ -36,6 +36,7 @@ mod tests {
|
||||||
#[test_case(Rule::SSHNoHostKeyVerification, Path::new("S507.py"))]
|
#[test_case(Rule::SSHNoHostKeyVerification, Path::new("S507.py"))]
|
||||||
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))]
|
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))]
|
||||||
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.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::SslWithNoVersion, Path::new("S504.py"))]
|
||||||
#[test_case(Rule::StartProcessWithAShell, Path::new("S605.py"))]
|
#[test_case(Rule::StartProcessWithAShell, Path::new("S605.py"))]
|
||||||
#[test_case(Rule::StartProcessWithNoShell, Path::new("S606.py"))]
|
#[test_case(Rule::StartProcessWithNoShell, Path::new("S606.py"))]
|
||||||
|
|
|
@ -20,6 +20,7 @@ pub(crate) use shell_injection::*;
|
||||||
pub(crate) use snmp_insecure_version::*;
|
pub(crate) use snmp_insecure_version::*;
|
||||||
pub(crate) use snmp_weak_cryptography::*;
|
pub(crate) use snmp_weak_cryptography::*;
|
||||||
pub(crate) use ssh_no_host_key_verification::*;
|
pub(crate) use ssh_no_host_key_verification::*;
|
||||||
|
pub(crate) use ssl_insecure_version::*;
|
||||||
pub(crate) use ssl_with_no_version::*;
|
pub(crate) use ssl_with_no_version::*;
|
||||||
pub(crate) use suspicious_function_call::*;
|
pub(crate) use suspicious_function_call::*;
|
||||||
pub(crate) use suspicious_imports::*;
|
pub(crate) use suspicious_imports::*;
|
||||||
|
@ -51,6 +52,7 @@ mod shell_injection;
|
||||||
mod snmp_insecure_version;
|
mod snmp_insecure_version;
|
||||||
mod snmp_weak_cryptography;
|
mod snmp_weak_cryptography;
|
||||||
mod ssh_no_host_key_verification;
|
mod ssh_no_host_key_verification;
|
||||||
|
mod ssl_insecure_version;
|
||||||
mod ssl_with_no_version;
|
mod ssl_with_no_version;
|
||||||
mod suspicious_function_call;
|
mod suspicious_function_call;
|
||||||
mod suspicious_imports;
|
mod suspicious_imports;
|
||||||
|
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -3512,6 +3512,7 @@
|
||||||
"S5",
|
"S5",
|
||||||
"S50",
|
"S50",
|
||||||
"S501",
|
"S501",
|
||||||
|
"S502",
|
||||||
"S504",
|
"S504",
|
||||||
"S505",
|
"S505",
|
||||||
"S506",
|
"S506",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue