From 33e62525ff5ffd3ccfa810e566f54a27dbcf50b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:51:33 +0000 Subject: [PATCH 1/9] Initial plan From 58f7521e2749f224d922a573d93aa11e8e886bb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:25:34 +0000 Subject: [PATCH 2/9] Detect URL-encoded CRLF (%0D%0A) in FtpWebRequest URI Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../src/System/Net/FtpWebRequest.cs | 3 ++- .../System.Net.Requests/tests/FtpWebRequestTest.cs | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Requests/src/System/Net/FtpWebRequest.cs b/src/libraries/System.Net.Requests/src/System/Net/FtpWebRequest.cs index f470879e993e41..9d55ba3baef617 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/FtpWebRequest.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/FtpWebRequest.cs @@ -487,7 +487,8 @@ internal FtpWebRequest(Uri uri) if ((object)uri.Scheme != (object)Uri.UriSchemeFtp) throw new ArgumentOutOfRangeException(nameof(uri)); - if (uri.OriginalString.Contains("\r\n", StringComparison.Ordinal)) + if (uri.OriginalString.Contains("\r\n", StringComparison.Ordinal) || + uri.OriginalString.Contains("%0D%0A", StringComparison.OrdinalIgnoreCase)) throw new FormatException(SR.net_ftp_no_newlines); _timerCallback = new TimerThread.Callback(TimerCallback); diff --git a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs index 5401b1e93ebbb7..cbdd492b2125cd 100644 --- a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs @@ -29,6 +29,17 @@ public FtpExecutionMode(bool useSsl, bool usePassive, bool useAsync, bool useOld } } + [Theory] + [InlineData("ftp://foo.com/bar\r\nbaz")] + [InlineData("ftp://foo.com/bar%0D%0Abaz")] + [InlineData("ftp://foo.com/bar%0d%0abaz")] + [InlineData("ftp://foo.com/bar%0D%0abaz")] + public void Ctor_NewLineInUri_ThrowsFormatException(string uriString) + { + Uri uri = new Uri(uriString, UriKind.Absolute); + Assert.Throws(() => WebRequest.Create(uri)); + } + [Fact] public void Ctor_VerifyDefaults_Success() { From de623651bf9e4107d3a2b0bfedc23b95a6b44d89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:14:22 +0000 Subject: [PATCH 3/9] Check \r, \n, %0D, %0A individually in FtpWebRequest URI Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../System.Net.Requests/src/System/Net/FtpWebRequest.cs | 5 +++-- .../System.Net.Requests/tests/FtpWebRequestTest.cs | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Requests/src/System/Net/FtpWebRequest.cs b/src/libraries/System.Net.Requests/src/System/Net/FtpWebRequest.cs index 9d55ba3baef617..91b95a5eea6604 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/FtpWebRequest.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/FtpWebRequest.cs @@ -487,8 +487,9 @@ internal FtpWebRequest(Uri uri) if ((object)uri.Scheme != (object)Uri.UriSchemeFtp) throw new ArgumentOutOfRangeException(nameof(uri)); - if (uri.OriginalString.Contains("\r\n", StringComparison.Ordinal) || - uri.OriginalString.Contains("%0D%0A", StringComparison.OrdinalIgnoreCase)) + if (uri.OriginalString.AsSpan().ContainsAny('\r', '\n') || + uri.OriginalString.Contains("%0A", StringComparison.OrdinalIgnoreCase) || + uri.OriginalString.Contains("%0D", StringComparison.OrdinalIgnoreCase)) throw new FormatException(SR.net_ftp_no_newlines); _timerCallback = new TimerThread.Callback(TimerCallback); diff --git a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs index cbdd492b2125cd..d1c28b921ebc8e 100644 --- a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs @@ -31,9 +31,15 @@ public FtpExecutionMode(bool useSsl, bool usePassive, bool useAsync, bool useOld [Theory] [InlineData("ftp://foo.com/bar\r\nbaz")] + [InlineData("ftp://foo.com/bar\rbaz")] + [InlineData("ftp://foo.com/bar\nbaz")] [InlineData("ftp://foo.com/bar%0D%0Abaz")] [InlineData("ftp://foo.com/bar%0d%0abaz")] [InlineData("ftp://foo.com/bar%0D%0abaz")] + [InlineData("ftp://foo.com/bar%0Dbaz")] + [InlineData("ftp://foo.com/bar%0dbaz")] + [InlineData("ftp://foo.com/bar%0Abaz")] + [InlineData("ftp://foo.com/bar%0abaz")] public void Ctor_NewLineInUri_ThrowsFormatException(string uriString) { Uri uri = new Uri(uriString, UriKind.Absolute); From ed3d92cc217b58c5c1ec5fa88bd95e1df2d6b496 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:46:39 +0000 Subject: [PATCH 4/9] Apply same \r/\n/%0D/%0A check to FormatFtpCommand Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../System.Net.Requests/src/System/Net/FtpControlStream.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs b/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs index ba72c6f8a9dc76..fe14c25c186f3c 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs @@ -1147,7 +1147,10 @@ private string GetPortCommandLine() /// private static string FormatFtpCommand(string command, string? parameter) { - if (parameter is not null && parameter.Contains("\r\n", StringComparison.Ordinal)) + if (parameter is not null && + (parameter.AsSpan().ContainsAny('\r', '\n') || + parameter.Contains("%0A", StringComparison.OrdinalIgnoreCase) || + parameter.Contains("%0D", StringComparison.OrdinalIgnoreCase))) { throw new FormatException(SR.net_ftp_no_newlines); } From f0cdd1dfeebfc041da0b094d0f8f04f076e7e211 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:24:16 +0000 Subject: [PATCH 5/9] Add test for FormatFtpCommand newline check via RenameTo Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../tests/FtpWebRequestTest.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs index d1c28b921ebc8e..90bedc6eef9eea 100644 --- a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Sockets; using System.Text; using System.Threading.Tasks; @@ -241,6 +242,48 @@ public void Ftp_Ignore_NewLine_GetRequestStream_And_GetResponse_Throws_FormatExc Assert.True(responseException.InnerException is FormatException); } + [Theory] + [InlineData("ok\r\nbad")] + [InlineData("ok\rbad")] + [InlineData("ok\nbad")] + [InlineData("ok%0D%0Abad")] + [InlineData("ok%0d%0abad")] + [InlineData("ok%0Dbad")] + [InlineData("ok%0dbad")] + [InlineData("ok%0Abad")] + [InlineData("ok%0abad")] + public void Ftp_NewLineInRenameTo_GetResponse_Throws_FormatException_As_InnerException(string renameTo) + { + // FormatFtpCommand is invoked when the command pipeline is built (in FtpControlStream.BuildCommandsList), + // which happens after the TCP connection is established but before any bytes are read from or written to + // the server. A minimal loopback listener that accepts the connection is therefore sufficient to drive + // the request to the point where the RNTO parameter is validated. + using TcpListener listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + + Task acceptTask = Task.Run(async () => + { + try + { + using TcpClient client = await listener.AcceptTcpClientAsync(); + // Keep the connection open briefly so the client can observe the validation failure + // before the socket is torn down. + await Task.Delay(1000); + } + catch { } + }); + + FtpWebRequest request = (FtpWebRequest)WebRequest.Create($"ftp://127.0.0.1:{port}/file"); + request.Method = WebRequestMethods.Ftp.Rename; + request.RenameTo = renameTo; + request.Timeout = 10_000; + request.ReadWriteTimeout = 10_000; + + WebException ex = Assert.Throws(() => request.GetResponse()); + Assert.IsType(ex.InnerException); + } + private static async Task DoAsync(FtpWebRequest request, MemoryStream requestBody) { if (requestBody != null) From ee0ba6f09c381fbc0e5b4b824f96a664c2c04e40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:57:56 +0000 Subject: [PATCH 6/9] Align RenameTo newline test with LocalServerAvailable/absoluteUri pattern Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../tests/FtpWebRequestTest.cs | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs index 90bedc6eef9eea..ba1a9d3f0e727f 100644 --- a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Sockets; using System.Text; using System.Threading.Tasks; @@ -242,7 +241,7 @@ public void Ftp_Ignore_NewLine_GetRequestStream_And_GetResponse_Throws_FormatExc Assert.True(responseException.InnerException is FormatException); } - [Theory] + [ConditionalTheory(typeof(FtpWebRequestTest), nameof(LocalServerAvailable))] [InlineData("ok\r\nbad")] [InlineData("ok\rbad")] [InlineData("ok\nbad")] @@ -254,31 +253,9 @@ public void Ftp_Ignore_NewLine_GetRequestStream_And_GetResponse_Throws_FormatExc [InlineData("ok%0abad")] public void Ftp_NewLineInRenameTo_GetResponse_Throws_FormatException_As_InnerException(string renameTo) { - // FormatFtpCommand is invoked when the command pipeline is built (in FtpControlStream.BuildCommandsList), - // which happens after the TCP connection is established but before any bytes are read from or written to - // the server. A minimal loopback listener that accepts the connection is therefore sufficient to drive - // the request to the point where the RNTO parameter is validated. - using TcpListener listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - int port = ((IPEndPoint)listener.LocalEndpoint).Port; - - Task acceptTask = Task.Run(async () => - { - try - { - using TcpClient client = await listener.AcceptTcpClientAsync(); - // Keep the connection open briefly so the client can observe the validation failure - // before the socket is torn down. - await Task.Delay(1000); - } - catch { } - }); - - FtpWebRequest request = (FtpWebRequest)WebRequest.Create($"ftp://127.0.0.1:{port}/file"); + FtpWebRequest request = (FtpWebRequest)WebRequest.Create(absoluteUri + Guid.NewGuid().ToString()); request.Method = WebRequestMethods.Ftp.Rename; request.RenameTo = renameTo; - request.Timeout = 10_000; - request.ReadWriteTimeout = 10_000; WebException ex = Assert.Throws(() => request.GetResponse()); Assert.IsType(ex.InnerException); From 7096392709a812d295c0b2e186d82e5a4ff8bf76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:24:41 +0000 Subject: [PATCH 7/9] Extend Ftp_Ignore_NewLine credential test with per-character variants Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../tests/FtpWebRequestTest.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs index ba1a9d3f0e727f..bad6610272b940 100644 --- a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs @@ -228,12 +228,21 @@ public void Ftp_Ignore_NewLine_Constructor_Throws_FormatException() Assert.Throws(() => WebRequest.Create($"{uri}\r\n{WebRequestMethods.Ftp.AppendFile} {Guid.NewGuid().ToString()}")); } - [ConditionalFact(typeof(FtpWebRequestTest), nameof(LocalServerAvailable))] - public void Ftp_Ignore_NewLine_GetRequestStream_And_GetResponse_Throws_FormatException_As_InnerException() + [ConditionalTheory(typeof(FtpWebRequestTest), nameof(LocalServerAvailable))] + [InlineData("test\r\ntest2")] + [InlineData("test\rtest2")] + [InlineData("test\ntest2")] + [InlineData("test%0D%0Atest2")] + [InlineData("test%0d%0atest2")] + [InlineData("test%0Dtest2")] + [InlineData("test%0dtest2")] + [InlineData("test%0Atest2")] + [InlineData("test%0atest2")] + public void Ftp_Ignore_NewLine_GetRequestStream_And_GetResponse_Throws_FormatException_As_InnerException(string credential) { FtpWebRequest ftpWebRequest = (FtpWebRequest)WebRequest.Create(absoluteUri + Guid.NewGuid().ToString()); ftpWebRequest.Method = "APPE"; - ftpWebRequest.Credentials = new NetworkCredential("test\r\ntest2", "test\r\ntest2"); + ftpWebRequest.Credentials = new NetworkCredential(credential, credential); var requestException = Assert.Throws(() => ftpWebRequest.GetRequestStream()); Assert.True(requestException.InnerException is FormatException); From fecf09ebf7b226e5e2e1a1af999cc8a59ee91643 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Jun 2026 13:50:52 +0000 Subject: [PATCH 8/9] Update net_ftp_no_newlines message to remove word "pair" Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- src/libraries/System.Net.Requests/src/Resources/Strings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Requests/src/Resources/Strings.resx b/src/libraries/System.Net.Requests/src/Resources/Strings.resx index e746e70fefa499..4919a50fbaa0a8 100644 --- a/src/libraries/System.Net.Requests/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Requests/src/Resources/Strings.resx @@ -196,7 +196,7 @@ The underlying connection was closed: An unexpected error occurred on a receive - CRLF character pair is not allowed in FtpWebRequest inputs. + CR and LF characters are not allowed in FtpWebRequest inputs. The remote name could not be resolved From c13f39c8da3904554882b3591dd9c510a941799b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:01:43 +0000 Subject: [PATCH 9/9] Remove %0D/%0A checks from FormatFtpCommand - only check literal CR/LF Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../src/System/Net/FtpControlStream.cs | 5 +---- .../System.Net.Requests/tests/FtpWebRequestTest.cs | 12 ------------ 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs b/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs index fe14c25c186f3c..6673d8483ec158 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs @@ -1147,10 +1147,7 @@ private string GetPortCommandLine() /// private static string FormatFtpCommand(string command, string? parameter) { - if (parameter is not null && - (parameter.AsSpan().ContainsAny('\r', '\n') || - parameter.Contains("%0A", StringComparison.OrdinalIgnoreCase) || - parameter.Contains("%0D", StringComparison.OrdinalIgnoreCase))) + if (parameter is not null && parameter.AsSpan().ContainsAny('\r', '\n')) { throw new FormatException(SR.net_ftp_no_newlines); } diff --git a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs index bad6610272b940..aae99260d14301 100644 --- a/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/FtpWebRequestTest.cs @@ -232,12 +232,6 @@ public void Ftp_Ignore_NewLine_Constructor_Throws_FormatException() [InlineData("test\r\ntest2")] [InlineData("test\rtest2")] [InlineData("test\ntest2")] - [InlineData("test%0D%0Atest2")] - [InlineData("test%0d%0atest2")] - [InlineData("test%0Dtest2")] - [InlineData("test%0dtest2")] - [InlineData("test%0Atest2")] - [InlineData("test%0atest2")] public void Ftp_Ignore_NewLine_GetRequestStream_And_GetResponse_Throws_FormatException_As_InnerException(string credential) { FtpWebRequest ftpWebRequest = (FtpWebRequest)WebRequest.Create(absoluteUri + Guid.NewGuid().ToString()); @@ -254,12 +248,6 @@ public void Ftp_Ignore_NewLine_GetRequestStream_And_GetResponse_Throws_FormatExc [InlineData("ok\r\nbad")] [InlineData("ok\rbad")] [InlineData("ok\nbad")] - [InlineData("ok%0D%0Abad")] - [InlineData("ok%0d%0abad")] - [InlineData("ok%0Dbad")] - [InlineData("ok%0dbad")] - [InlineData("ok%0Abad")] - [InlineData("ok%0abad")] public void Ftp_NewLineInRenameTo_GetResponse_Throws_FormatException_As_InnerException(string renameTo) { FtpWebRequest request = (FtpWebRequest)WebRequest.Create(absoluteUri + Guid.NewGuid().ToString());