fix: reject empty HELO/EHLO with 501 per RFC 5321

EHLO/HELO require a domain or address-literal argument. Previously
the server accepted bare EHLO with no argument and responded 250,
which deviates from the spec and makes the honeypot easier to
fingerprint.
This commit is contained in:
2026-04-14 00:30:46 -04:00
parent c2f7622fbb
commit 5631d09aa8
2 changed files with 19 additions and 0 deletions

View File

@@ -142,6 +142,11 @@ class SMTPProtocol(asyncio.Protocol):
args = parts[1] if len(parts) > 1 else "" args = parts[1] if len(parts) > 1 else ""
if cmd in ("EHLO", "HELO"): if cmd in ("EHLO", "HELO"):
if not args:
self._transport.write(
f"501 5.5.4 Syntax: {cmd} hostname\r\n".encode()
)
return
_log("ehlo", src=self._peer[0], domain=args) _log("ehlo", src=self._peer[0], domain=args)
self._transport.write( self._transport.write(
f"250-{_SMTP_MTA}\r\n" f"250-{_SMTP_MTA}\r\n"

View File

@@ -114,6 +114,20 @@ def test_ehlo_returns_250_multiline(relay_mod):
assert "PIPELINING" in combined assert "PIPELINING" in combined
def test_ehlo_empty_domain_rejected(relay_mod):
proto, _, written = _make_protocol(relay_mod)
_send(proto, "EHLO")
replies = _replies(written)
assert any(r.startswith("501") for r in replies)
def test_helo_empty_domain_rejected(relay_mod):
proto, _, written = _make_protocol(relay_mod)
_send(proto, "HELO")
replies = _replies(written)
assert any(r.startswith("501") for r in replies)
# ── OPEN RELAY MODE ─────────────────────────────────────────────────────────── # ── OPEN RELAY MODE ───────────────────────────────────────────────────────────
class TestOpenRelay: class TestOpenRelay: