Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions src/tls_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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 ─────────────────────────────────────────────────────────────────────
Expand Down
31 changes: 30 additions & 1 deletion test_tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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():
Expand Down
Loading