diff --git a/src/Renci.SshNet/ScpClient.cs b/src/Renci.SshNet/ScpClient.cs index 20704502..a630bb18 100644 --- a/src/Renci.SshNet/ScpClient.cs +++ b/src/Renci.SshNet/ScpClient.cs @@ -5,6 +5,9 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Net; +#if NET +using System.Runtime.InteropServices; +#endif using System.Text.RegularExpressions; using System.Threading; @@ -86,6 +89,12 @@ namespace Renci.SshNet private static readonly Regex TimestampRegex = new Regex(TimestampPattern, RegexOptions.Compiled); #endif + /// + /// The maximum length, in bytes and excluding the terminating line feed, of a control line + /// or error message received from the server. + /// + internal const int MaxControlLineLength = 64 * 1024; + private static readonly byte[] SuccessConfirmationCode = { 0 }; private static readonly byte[] ErrorConfirmationCode = { 1 }; @@ -753,18 +762,25 @@ namespace Renci.SshNet while (b != SshNet.Session.LineFeed) { + if (buffer.Count == MaxControlLineLength) + { + throw new ScpException(string.Format("The server sent a line that exceeds the maximum length of {0} bytes.", MaxControlLineLength)); + } + buffer.Add((byte)b); b = ReadByte(stream); } - - var readBytes = buffer.ToArray(); - +#if NET + var line = ConnectionInfo.Encoding.GetString(CollectionsMarshal.AsSpan(buffer)); +#else + var line = ConnectionInfo.Encoding.GetString(buffer.ToArray(), 0, buffer.Count); +#endif if (hasError) { - throw new ScpException(ConnectionInfo.Encoding.GetString(readBytes, 0, readBytes.Length)); + throw new ScpException(line); } - return ConnectionInfo.Encoding.GetString(readBytes, 0, readBytes.Length); + return line; } /// diff --git a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_ControlLineTooLong.cs b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_ControlLineTooLong.cs new file mode 100644 index 00000000..ed9bd6df --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_ControlLineTooLong.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Renci.SshNet.Common; +using Renci.SshNet.Tests.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// Verifies that the length of a control line received from the server is bounded. + /// + [TestClass] + public class ScpClientTest_Download_ControlLineTooLong + { + [TestMethod] + public void LineLongerThanMaximum_ShouldThrowScpException() + { + var transcript = new ScpServerTranscript() + .Raw("C0644 1 " + new string('a', ScpClient.MaxControlLineLength)) + .Raw("\n"); + + using (var destination = new MemoryStream()) + { + var result = transcript.Run(client => client.Download("/remote/file", destination), closeChannel: true); + + Assert.IsTrue(result.Completed); + Assert.IsInstanceOfType(result.Exception); + Assert.Contains("maximum length", result.Exception.Message, StringComparison.Ordinal); + } + } + + [TestMethod] + public void LineOfMaximumLength_ShouldBeAccepted() + { + const string Header = "C0644 1 "; + + var transcript = new ScpServerTranscript() + .Line(Header + new string('a', ScpClient.MaxControlLineLength - Header.Length)) + .Raw("x") + .Raw([0]); + + using (var destination = new MemoryStream()) + { + var result = transcript.Run(client => client.Download("/remote/file", destination), closeChannel: true); + + Assert.IsTrue(result.Completed); + Assert.IsNull(result.Exception, result.Exception?.ToString()); + Assert.AreEqual(1, destination.Length); + } + } + } +}