Skip to content

Commit

Permalink
Add support for partial success limit to prevent stack overflow on ba…
Browse files Browse the repository at this point in the history
…dly behaving servers

Add support for partial success limit to prevent stack overflow on badly behaving servers
  • Loading branch information
drieseng authored Oct 5, 2017
2 parents 69fb3be + c2e1de5 commit 16004ef
Show file tree
Hide file tree
Showing 21 changed files with 1,746 additions and 147 deletions.
57 changes: 56 additions & 1 deletion src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,64 @@ public class ClientAuthenticationTest
[TestInitialize]
public void Init()
{
_clientAuthentication = new ClientAuthentication();
_clientAuthentication = new ClientAuthentication(1);
}

[TestMethod]
public void Ctor_PartialSuccessLimit_Zero()
{
const int partialSuccessLimit = 0;

try
{
new ClientAuthentication(partialSuccessLimit);
Assert.Fail();
}
catch (ArgumentOutOfRangeException ex)
{
Assert.IsNull(ex.InnerException);
Assert.AreEqual(string.Format("Cannot be less than one.{0}Parameter name: {1}", Environment.NewLine, ex.ParamName), ex.Message);
Assert.AreEqual("partialSuccessLimit", ex.ParamName);
}
}

[TestMethod]
public void Ctor_PartialSuccessLimit_Negative()
{
var partialSuccessLimit = new Random().Next(int.MinValue, -1);

try
{
new ClientAuthentication(partialSuccessLimit);
Assert.Fail();
}
catch (ArgumentOutOfRangeException ex)
{
Assert.IsNull(ex.InnerException);
Assert.AreEqual(string.Format("Cannot be less than one.{0}Parameter name: {1}", Environment.NewLine, ex.ParamName), ex.Message);
Assert.AreEqual("partialSuccessLimit", ex.ParamName);
}
}

[TestMethod]
public void Ctor_PartialSuccessLimit_One()
{
const int partialSuccessLimit = 1;

var clientAuthentication = new ClientAuthentication(partialSuccessLimit);
Assert.AreEqual(partialSuccessLimit, clientAuthentication.PartialSuccessLimit);
}

[TestMethod]
public void Ctor_PartialSuccessLimit_MaxValue()
{
const int partialSuccessLimit = int.MaxValue;

var clientAuthentication = new ClientAuthentication(partialSuccessLimit);
Assert.AreEqual(partialSuccessLimit, clientAuthentication.PartialSuccessLimit);
}


[TestMethod]
public void AuthenticateShouldThrowArgumentNullExceptionWhenConnectionInfoIsNull()
{
Expand Down
19 changes: 11 additions & 8 deletions src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public abstract class ClientAuthenticationTestBase : TestBase
internal Mock<IAuthenticationMethod> PasswordAuthenticationMethodMock { get; private set; }
internal Mock<IAuthenticationMethod> PublicKeyAuthenticationMethodMock { get; private set; }
internal Mock<IAuthenticationMethod> KeyboardInteractiveAuthenticationMethodMock { get; private set; }
internal ClientAuthentication ClientAuthentication { get; private set; }

protected abstract void SetupData();

protected void CreateMocks()
{
Expand All @@ -27,18 +28,20 @@ protected void CreateMocks()

protected abstract void SetupMocks();

protected virtual void Arrange()
{
SetupData();
CreateMocks();
SetupMocks();
}

protected abstract void Act();

protected override void OnInit()
protected sealed override void OnInit()
{
base.OnInit();

// Arrange
CreateMocks();
SetupMocks();
ClientAuthentication = new ClientAuthentication();

// Act
Arrange();
Act();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Renci.SshNet.Common;

namespace Renci.SshNet.Tests.Classes
{
/// <summary>
/// * ConnectionInfo provides the following authentication methods (in order):
/// o publickey
/// o password
/// * Partial success limit is 2
///
/// none
/// (1=FAIL)
/// |
/// +------------------------+------------------------+
/// | | |
/// password ◄--\ publickey keyboard-interactive
/// (7=SKIP) | (2=PS)
/// | |
/// | password
/// | (3=PS)
/// | |
/// | password
/// | (4=PS)
/// | |
/// | publickey
/// | (5=PS)
/// | |
/// \---- publickey
/// (6=SKIP)
/// </summary>
[TestClass]
public class ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit : ClientAuthenticationTestBase
{
private int _partialSuccessLimit;
private ClientAuthentication _clientAuthentication;
private SshAuthenticationException _actualException;

protected override void SetupData()
{
_partialSuccessLimit = 2;
}

protected override void SetupMocks()
{
var seq = new MockSequence();

SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER"));

ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
.Returns(NoneAuthenticationMethodMock.Object);

/* 1 */

NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
.Returns(AuthenticationResult.Failure);
ConnectionInfoMock.InSequence(seq)
.Setup(p => p.AuthenticationMethods)
.Returns(new List<IAuthenticationMethod>
{
PublicKeyAuthenticationMethodMock.Object,
PasswordAuthenticationMethodMock.Object
});
NoneAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.AllowedAuthentications)
.Returns(new[] {"password", "publickey", "keyboard-interactive"});

/* Enumerate supported authentication methods */

PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");

/* 2 */

PublicKeyAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.Authenticate(SessionMock.Object))
.Returns(AuthenticationResult.PartialSuccess);
PublicKeyAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.AllowedAuthentications)
.Returns(new[] {"password"});

/* Enumerate supported authentication methods */

PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");

/* 3 */

PasswordAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.Authenticate(SessionMock.Object))
.Returns(AuthenticationResult.PartialSuccess);
PasswordAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.AllowedAuthentications)
.Returns(new[] {"password"});

/* Enumerate supported authentication methods */

PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");

/* 4 */

PasswordAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.Authenticate(SessionMock.Object))
.Returns(AuthenticationResult.PartialSuccess);
PasswordAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.AllowedAuthentications)
.Returns(new[] {"publickey"});

/* Enumerate supported authentication methods */

PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");

/* 5 */

PublicKeyAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.Authenticate(SessionMock.Object))
.Returns(AuthenticationResult.PartialSuccess);
PublicKeyAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.AllowedAuthentications)
.Returns(new[] {"publickey"});

/* Enumerate supported authentication methods */

PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");

/* 6: Record partial success limit reached exception, and skip password authentication method */

PublicKeyAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.Name)
.Returns("publickey-partial1");

/* 7: Record partial success limit reached exception, and skip password authentication method */

PasswordAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.Name)
.Returns("password-partial1");

SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
}

protected override void Arrange()
{
base.Arrange();

_clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
}

protected override void Act()
{
try
{
_clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
Assert.Fail();
}
catch (SshAuthenticationException ex)
{
_actualException = ex;
}
}

[TestMethod]
public void AuthenticateOnPasswordAuthenticationMethodShouldHaveBeenInvokedTwice()
{
PasswordAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2));
}

[TestMethod]
public void AuthenticateOnPublicKeyAuthenticationMethodShouldHaveBeenInvokedTwice()
{
PublicKeyAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2));
}

[TestMethod]
public void AuthenticateShouldThrowSshAuthenticationException()
{
Assert.IsNotNull(_actualException);
Assert.IsNull(_actualException.InnerException);
Assert.AreEqual("Reached authentication attempt limit for method (password-partial1).", _actualException.Message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ namespace Renci.SshNet.Tests.Classes
[TestClass]
public class ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed : ClientAuthenticationTestBase
{
private int _partialSuccessLimit;
private ClientAuthentication _clientAuthentication;
private SshAuthenticationException _actualException;

protected override void SetupData()
{
_partialSuccessLimit = 1;
}

protected override void SetupMocks()
{
var seq = new MockSequence();
Expand All @@ -21,23 +28,26 @@ protected override void SetupMocks()
ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
.Returns(NoneAuthenticationMethodMock.Object);

NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
.Returns(AuthenticationResult.Failure);
ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods)
.Returns(new List<IAuthenticationMethod>
{
PublicKeyAuthenticationMethodMock.Object,
PasswordAuthenticationMethodMock.Object
});
NoneAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.AllowedAuthentications)
.Returns(new[] { "password" });
.Setup(p => p.Authenticate(SessionMock.Object))
.Returns(AuthenticationResult.Failure);
ConnectionInfoMock.InSequence(seq)
.Setup(p => p.AuthenticationMethods)
.Returns(new List<IAuthenticationMethod>
{
PublicKeyAuthenticationMethodMock.Object,
PasswordAuthenticationMethodMock.Object
});
NoneAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.AllowedAuthentications)
.Returns(new[] { "password" });

PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");

PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
.Returns(AuthenticationResult.Failure);
PasswordAuthenticationMethodMock.InSequence(seq)
.Setup(p => p.Authenticate(SessionMock.Object))
.Returns(AuthenticationResult.Failure);
// obtain name for inclusion in SshAuthenticationException
PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");

Expand All @@ -46,11 +56,18 @@ protected override void SetupMocks()
SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
}

protected override void Arrange()
{
base.Arrange();

_clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
}

protected override void Act()
{
try
{
ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
_clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
Assert.Fail();
}
catch (SshAuthenticationException ex)
Expand Down
Loading

0 comments on commit 16004ef

Please sign in to comment.