diff --git a/src/RestSharp/Parameters/HeaderParameter.cs b/src/RestSharp/Parameters/HeaderParameter.cs index 1607eaeda..7dce5df92 100644 --- a/src/RestSharp/Parameters/HeaderParameter.cs +++ b/src/RestSharp/Parameters/HeaderParameter.cs @@ -13,22 +13,71 @@ // limitations under the License. // +using System.Text; +using System.Text.RegularExpressions; + namespace RestSharp; -public record HeaderParameter : Parameter { +public partial record HeaderParameter : Parameter { /// /// Instantiates a header parameter /// - /// Parameter name - /// Parameter value - public HeaderParameter(string name, string value) + /// Header name + /// Header value + /// Set to true to encode header value according to RFC 2047. Default is false. + public HeaderParameter(string name, string value, bool encode = false) : base( - Ensure.NotEmptyString(name, nameof(name)), - Ensure.NotNull(value, nameof(value)), + EnsureValidHeaderString(Ensure.NotEmptyString(name, nameof(name)), "name"), + EnsureValidHeaderValue(name, value, encode), ParameterType.HttpHeader, false ) { } public new string Name => base.Name!; public new string Value => (string)base.Value!; + + static string EnsureValidHeaderValue(string name, string value, bool encode) { + CheckAndThrowsForInvalidHost(name, value); + + return EnsureValidHeaderString(GetValue(Ensure.NotNull(value, nameof(value)), encode), "value"); + } + + static string EnsureValidHeaderString(string value, string type) + => !IsInvalidHeaderString(value) ? value : throw new ArgumentException($"Invalid character found in header {type}: {value}"); + + static string GetValue(string value, bool encode) => encode ? GetBase64EncodedHeaderValue(value) : value; + + static string GetBase64EncodedHeaderValue(string value) => $"=?UTF-8?B?{Convert.ToBase64String(Encoding.UTF8.GetBytes(value))}?="; + + static bool IsInvalidHeaderString(string stringValue) { + // ReSharper disable once ForCanBeConvertedToForeach + for (var i = 0; i < stringValue.Length; i++) { + switch (stringValue[i]) { + case '\t': + case '\r': + case '\n': + return true; + } + } + + return false; + } + + static readonly Regex PortSplitRegex = PartSplit(); + + static void CheckAndThrowsForInvalidHost(string name, string value) { + if (name == KnownHeaders.Host && InvalidHost(value)) + throw new ArgumentException("The specified value is not a valid Host header string.", nameof(value)); + + return; + + static bool InvalidHost(string host) => Uri.CheckHostName(PortSplitRegex.Split(host)[0]) == UriHostNameType.Unknown; + } + +#if NET7_0_OR_GREATER + [GeneratedRegex(@":\d+")] + private static partial Regex PartSplit(); +#else + static Regex PartSplit() => new(@":\d+"); +#endif } \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.Headers.cs b/src/RestSharp/Request/RestRequestExtensions.Headers.cs index 8091a1a50..d3e6b7815 100644 --- a/src/RestSharp/Request/RestRequestExtensions.Headers.cs +++ b/src/RestSharp/Request/RestRequestExtensions.Headers.cs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Text.RegularExpressions; - namespace RestSharp; public static partial class RestRequestExtensions { @@ -39,10 +37,8 @@ public static RestRequest AddHeader(this RestRequest request, string name, strin /// Header name /// Header value /// - public static RestRequest AddHeader(this RestRequest request, string name, string value) { - CheckAndThrowsForInvalidHost(name, value); - return request.AddParameter(new HeaderParameter(name, value)); - } + public static RestRequest AddHeader(this RestRequest request, string name, string value) + => request.AddParameter(new HeaderParameter(name, value)); /// /// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource. @@ -62,10 +58,8 @@ public static RestRequest AddHeader(this RestRequest request, string name, T /// Header name /// Header value /// - public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, string value) { - CheckAndThrowsForInvalidHost(name, value); - return request.AddOrUpdateParameter(new HeaderParameter(name, value)); - } + public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, string value) + => request.AddOrUpdateParameter(new HeaderParameter(name, value)); /// /// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource. @@ -121,22 +115,4 @@ static void CheckAndThrowsDuplicateKeys(ICollection throw new ArgumentException($"Duplicate header names exist: {string.Join(", ", duplicateKeys)}"); } } - - static readonly Regex PortSplitRegex = PartSplit(); - - static void CheckAndThrowsForInvalidHost(string name, string value) { - if (name == KnownHeaders.Host && InvalidHost(value)) - throw new ArgumentException("The specified value is not a valid Host header string.", nameof(value)); - - return; - - static bool InvalidHost(string host) => Uri.CheckHostName(PortSplitRegex.Split(host)[0]) == UriHostNameType.Unknown; - } - -#if NET7_0_OR_GREATER - [GeneratedRegex(@":\d+")] - private static partial Regex PartSplit(); -#else - static Regex PartSplit() => new(@":\d+"); -#endif } \ No newline at end of file diff --git a/test/RestSharp.Tests/RequestHeaderTests.cs b/test/RestSharp.Tests/RequestHeaderTests.cs index 6fa6c8215..bf8bc4c4b 100644 --- a/test/RestSharp.Tests/RequestHeaderTests.cs +++ b/test/RestSharp.Tests/RequestHeaderTests.cs @@ -174,6 +174,12 @@ public void Should_not_allow_empty_header_name() { var request = new RestRequest(); Assert.Throws("name", () => request.AddHeader("", "value")); } + + [Fact] + public void Should_not_allow_CRLF_in_header_value() { + var request = new RestRequest(); + Assert.Throws(() => request.AddHeader("name", "test\r\nUser-Agent: injected header!\r\n\r\nGET /smuggled HTTP/1.1\r\nHost: insert.some.site.here")); + } static Parameter[] GetHeaders(RestRequest request) => request.Parameters.Where(x => x.Type == ParameterType.HttpHeader).ToArray();