Skip to content

Commit

Permalink
Improve robustness of SMTP status code parser
Browse files Browse the repository at this point in the history
  • Loading branch information
jstedfast committed Jan 16, 2024
1 parent 14272a6 commit fd304da
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 9 deletions.
33 changes: 27 additions & 6 deletions MailKit/Net/Smtp/SmtpStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -447,21 +447,42 @@ public override Task<int> ReadAsync (byte[] buffer, int offset, int count, Cance
#endif
}

static bool TryParseStatusCode (byte[] text, int startIndex, out int code)
{
int endIndex = startIndex + 3;

code = 0;

for (int index = startIndex; index < endIndex; index++) {
if (text[index] < (byte) '0' || text[index] > (byte) '9')
return false;

int digit = text[index] - (byte) '0';
code = (code * 10) + digit;
}

return true;
}

static bool IsLegalAfterStatusCode (byte c)
{
return c == (byte) '-' || c == (byte) ' ' || c == (byte) '\r' || c == (byte) '\n';
}

bool ReadResponse (ByteArrayBuilder builder, ref bool newLine, ref bool more, ref int code)
{
do {
int startIndex = inputIndex;

if (newLine) {
if (inputIndex + 3 < inputEnd) {
if (!ByteArrayBuilder.TryParse (input, ref inputIndex, inputEnd, out int value))
if (!TryParseStatusCode (input, inputIndex, out int value))
throw new SmtpProtocolException ("Unable to parse status code returned by the server.");

if (inputIndex == inputEnd) {
// Need input.
inputIndex = startIndex;
return true;
}
inputIndex += 3;

if (value < 100 || !IsLegalAfterStatusCode (input[inputIndex]))
throw new SmtpProtocolException ("Invalid status code returned by the server.");

if (code == 0) {
code = value;
Expand Down
67 changes: 64 additions & 3 deletions UnitTests/Net/Smtp/SmtpStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,15 @@ public void TestRead ()
}
}

[Test]
public void TestReadResponseInvalidResponseCode ()
[TestCase ("XXX")]
[TestCase ("0")]
[TestCase ("01")]
[TestCase ("012")]
[TestCase ("1234")]
public void TestReadResponseInvalidStatusCode (string statusCode)
{
using (var stream = new SmtpStream (new DummyNetworkStream (), new NullProtocolLogger ())) {
var buffer = Encoding.ASCII.GetBytes ("XXX This is an invalid response.\r\n");
var buffer = Encoding.ASCII.GetBytes ($"{statusCode} This is an invalid response.\r\n");
var dummy = (MemoryStream) stream.Stream;

dummy.Write (buffer, 0, buffer.Length);
Expand All @@ -82,6 +86,63 @@ public void TestReadResponseInvalidResponseCode ()
}
}

static string GenerateCrossBoundaryResponse (int statusCodeUnderflow)
{
const string lastLine = "250 ...And this is the final line of the response.\r\n";
var builder = new StringBuilder ();
int lineNumber = 1;
string line;

do {
line = $"250-This is line #{lineNumber++} of a really long SMTP response.\r\n";

if (builder.Length + line.Length + 6 + statusCodeUnderflow > 4096)
break;

builder.Append (line);
} while (true);

line = "250-" + new string ('a', 4096 - builder.Length - 6 - statusCodeUnderflow) + "\r\n";
builder.Append (line);
builder.Append (lastLine);

// At this point, the last line's status code (and the following <SPACE>) is just barely
// contained within the first 4096 byte read.

var input = builder.ToString ();

var expected = lastLine.Substring (statusCodeUnderflow);
var buffer2 = input.Substring (4096);

Assert.That (buffer2, Is.EqualTo (expected));

return input;
}

[TestCase (0)]
[TestCase (1)]
[TestCase (2)]
[TestCase (3)]
[TestCase (4)]
public void TestReadResponseStatusCodeUnderflow (int underflow)
{
var input = GenerateCrossBoundaryResponse (underflow);
var expected = input.Replace ("250-", "").Replace ("250 ", "").Replace ("\r\n", "\n").TrimEnd ();

using (var stream = new SmtpStream (new DummyNetworkStream (), new NullProtocolLogger ())) {
var buffer = Encoding.ASCII.GetBytes (input);
var dummy = (MemoryStream) stream.Stream;

dummy.Write (buffer, 0, buffer.Length);
dummy.Position = 0;

var response = stream.ReadResponse (CancellationToken.None);

Assert.That ((int) response.StatusCode, Is.EqualTo (250));
Assert.That (response.Response, Is.EqualTo (expected));
}
}

[Test]
public void TestReadResponseMismatchedResponseCodes ()
{
Expand Down

0 comments on commit fd304da

Please sign in to comment.