diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs index cdd6ecac601d..3fbcc4854f42 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs @@ -98,7 +98,13 @@ private void AppendContentLength(ReadOnlySpan value) KestrelBadHttpRequestException.Throw(RequestRejectionReason.MultipleContentLengths); } - if (!Utf8Parser.TryParse(value, out long parsed, out var consumed) || + // Per RFC 9110 §8.6, Content-Length is 1*DIGIT: only ASCII digits are allowed. + // https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6 + // Utf8Parser.TryParse accepts a leading '+' or '-' sign, so ensure the first byte is a digit. + long parsed = 0; + if (value.IsEmpty || + value[0] is < (byte)'0' or > (byte)'9' || + !Utf8Parser.TryParse(value, out parsed, out var consumed) || parsed < 0 || consumed != value.Length) { @@ -122,8 +128,11 @@ private unsafe void AppendContentLengthCustomEncoding(ReadOnlySpan value, var numChars = customEncoding.GetChars(value, decodedChars); long parsed = -1; + // Per RFC 9110 §8.6, Content-Length is 1*DIGIT. NumberStyles.None + // https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6 + // disallows leading signs and whitespace that NumberStyles.Integer would accept. if (numChars > 19 || - !long.TryParse(decodedChars.Slice(0, numChars), NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) || + !long.TryParse(decodedChars.Slice(0, numChars), NumberStyles.None, CultureInfo.InvariantCulture, out parsed) || parsed < 0) { KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetRequestHeaderString(HeaderNames.ContentLength, EncodingSelector, checkForNewlineChars: false)); diff --git a/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs b/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs index 7a9b6dea0f78..66aca720f271 100644 --- a/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs @@ -710,6 +710,27 @@ public void CanSpecifyEncodingForContentLength() new HttpRequestHeaders().Append(contentLengthNameBytes, contentLengthValueBytes, checkForNewlineChars: false)); } + [Theory] + [InlineData("+1")] + [InlineData("+0")] + [InlineData("-0")] + [InlineData("-1")] + [InlineData(" 1")] + [InlineData("1a")] + [InlineData("")] + public void ContentLengthWithCustomEncodingRejectsNonDigits(string value) + { + var contentLengthNameBytes = Encoding.ASCII.GetBytes(HeaderNames.ContentLength); + var contentLengthValueBytes = Encoding.UTF32.GetBytes(value); + + var headers = new HttpRequestHeaders(encodingSelector: _ => Encoding.UTF32); + +#pragma warning disable CS0618 // Type or member is obsolete + Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete + headers.Append(contentLengthNameBytes, contentLengthValueBytes, checkForNewlineChars: false)); + } + [Fact] public void ValueReuseNeverWhenUnknownHeader() { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs index 05481529b5ea..cb49b2822307 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs @@ -100,6 +100,10 @@ public Task BadRequestIfMethodRequiresLengthButNoContentLengthInHttp10Request(st [Theory] [InlineData("NaN")] [InlineData("-1")] + [InlineData("+1")] + [InlineData("+0")] + [InlineData("-0")] + [InlineData("")] public Task BadRequestIfContentLengthInvalid(string contentLength) { return TestBadRequest(