From 63431edbeeff53c497eabfed836ff07fceb7adad Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Sep 2026 11:08:29 +0000 Subject: [PATCH] Don't report DROWN from TLS 1.0 and 1.1 alone. CVE-2016-0800 requires SSLv2. The analyzer cannot negotiate SSLv2, so treating any two deprecated protocols as confirmed DROWN marked typical TLS 1.0+1.1 hosts CRITICAL with grade F. Only emit DROWN when SSLv2 is actually in the deprecated list. Co-authored-by: dmitryflynn --- src/tls_analyzer.py | 30 +++++++++++++++++------------- test_tls.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/tls_analyzer.py b/src/tls_analyzer.py index 7e9dc3a..5c2185d 100644 --- a/src/tls_analyzer.py +++ b/src/tls_analyzer.py @@ -146,8 +146,7 @@ def probe_protocols(host: str, port: int) -> tuple[list[str], list[str]]: ``deprecated`` contains only protocols whose support was *confirmed* by a successful, version-verified handshake — never an untestable placeholder. - This keeps downstream counts (DROWN heuristic, grading) free of false - positives. + This keeps grading (and any SSLv2-gated checks) free of false positives. """ supported = [] deprecated = [] @@ -410,17 +409,22 @@ def check_crime(sock: ssl.SSLSocket) -> Optional[TLSFinding]: return None def check_drown(deprecated: list[str]) -> Optional[TLSFinding]: - """DROWN: SSLv2 support on same key.""" - # Can't reliably test SSLv2 from Python; flag if very old protocols found - if len(deprecated) >= 2: - return TLSFinding( - severity="CRITICAL", cvss=9.8, cve="CVE-2016-0800", - title="DROWN — Decrypting RSA with Obsolete and Weakened eNcryption", - detail="Multiple deprecated protocol versions detected. If SSLv2 is also " - "enabled on this host or shares a key with an SSLv2-enabled server, " - "DROWN attack can decrypt RSA-encrypted TLS sessions." - ) - return None + """DROWN (CVE-2016-0800) requires SSLv2 on the host or a shared RSA key. + + Python cannot negotiate SSLv2, so this check must not infer DROWN from + TLS 1.0+TLS 1.1 (the only deprecated versions we can actually probe). + Those are common on legacy stacks and are already reported as MEDIUM + deprecated-protocol findings. Treating them as confirmed DROWN produced + a false CRITICAL / grade F on almost every dual-stack TLS 1.0/1.1 host. + """ + if "SSLv2" not in deprecated: + return None + return TLSFinding( + severity="CRITICAL", cvss=9.8, cve="CVE-2016-0800", + title="DROWN — Decrypting RSA with Obsolete and Weakened eNcryption", + detail="SSLv2 is enabled. DROWN can decrypt RSA-encrypted TLS sessions " + "that share this key, including modern TLS connections." + ) # ─── Grading ───────────────────────────────────────────────────────────────────── diff --git a/test_tls.py b/test_tls.py index a0f52e8..043ea10 100644 --- a/test_tls.py +++ b/test_tls.py @@ -240,7 +240,7 @@ def test_analyze_cipher_empty(): def test_probe_deprecated_excludes_placeholder(monkeypatch): # Force the "missing constant" path for TLSv1 and ensure it does NOT leak # an untestable placeholder into the deprecated list (which would inflate - # the DROWN heuristic and grading). + # grading). monkeypatch.setitem(tls._PROTO_MAP, "TLSv1", None) # Pretend nothing else negotiates either, to isolate the placeholder path. monkeypatch.setattr(tls, "_try_connect", lambda *a, **k: None) @@ -260,6 +260,35 @@ def fake_connect(host, port, min_v, max_v, timeout=5.0, expected_version=None): assert "TLSv1.2" in supported and "TLSv1.3" in supported +# ─── DROWN must not fire on TLS 1.0+1.1 (CVE-2016-0800 requires SSLv2) ───────── + +def test_drown_not_inferred_from_tls10_and_tls11(): + # The common legacy stack that still offers TLS 1.0 and 1.1. probe_protocols + # puts both in `deprecated`; that must not become a confirmed DROWN CRITICAL. + assert tls.check_drown(["TLSv1.0", "TLSv1.1"]) is None + assert tls.check_drown(["TLSv1.0"]) is None + assert tls.check_drown([]) is None + + +def test_drown_only_when_sslv2_confirmed(): + finding = tls.check_drown(["SSLv2", "TLSv1.0"]) + assert finding is not None + assert finding.severity == "CRITICAL" + assert finding.cve == "CVE-2016-0800" + + +def test_probe_tls10_tls11_does_not_emit_drown(monkeypatch): + """Full analyzer path: a host that speaks TLS 1.0+1.1 (and 1.2) is not DROWN.""" + def fake_connect(host, port, min_v, max_v, timeout=5.0, expected_version=None): + return expected_version + monkeypatch.setattr(tls, "_try_connect", fake_connect) + _supported, deprecated = tls.probe_protocols("198.51.100.1", 443) + finding = tls.check_drown(deprecated) + assert finding is None + # Grade without the false CRITICAL is D (two deprecated protocols), not F. + assert tls.calculate_grade([], deprecated, None) == "D" + + # ─── grading ──────────────────────────────────────────────────────────────────── def test_grade_clean_is_a():