Limit the length of SCP control lines

ReadString accumulated bytes until a line feed with no upper bound, so a
server could exhaust client memory by never sending one. Control lines and
error messages are now limited to 64 KiB; a longer line throws ScpException.
This commit is contained in:
Rob Hague
2026-10-01 21:00:57 +00:00
parent 5ef2f6ce69
commit ece424a85e
2 changed files with 75 additions and 5 deletions
+21 -5
View File
@@ -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
/// <summary>
/// The maximum length, in bytes and excluding the terminating line feed, of a control line
/// or error message received from the server.
/// </summary>
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;
}
/// <summary>
@@ -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
{
/// <summary>
/// Verifies that the length of a control line received from the server is bounded.
/// </summary>
[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<ScpException>(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);
}
}
}
}