| | | 1 | | // Licensed to the .NET Foundation under one or more agreements. |
| | | 2 | | // The .NET Foundation licenses this file to you under the MIT license. |
| | | 3 | | |
| | | 4 | | using System.Net.Http.Headers; |
| | | 5 | | using System.Runtime.InteropServices; |
| | | 6 | | |
| | | 7 | | namespace DotnetFuzzing.Fuzzers; |
| | | 8 | | |
| | | 9 | | internal sealed class HttpHeadersFuzzer : IFuzzer |
| | | 10 | | { |
| | 1 | 11 | | public string[] TargetAssemblies { get; } = ["System.Net.Http"]; |
| | 0 | 12 | | public string[] TargetCoreLibPrefixes => []; |
| | | 13 | | |
| | | 14 | | // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeader |
| | 1 | 15 | | private static readonly string[] s_knownHeaderNames = [ |
| | 1 | 16 | | ":status", "Accept", "Accept-Charset", "Accept-Encoding", "Accept-Language", "Accept-Patch", |
| | 1 | 17 | | "Accept-Ranges", "Access-Control-Allow-Credentials", "Access-Control-Allow-Headers", |
| | 1 | 18 | | "Access-Control-Allow-Methods", "Access-Control-Allow-Origin", "Access-Control-Expose-Headers", |
| | 1 | 19 | | "Access-Control-Max-Age", "Age", "Allow", "Alt-Svc", "Alt-Used", "Authorization", "Cache-Control", |
| | 1 | 20 | | "Connection", "Content-Disposition", "Content-Encoding", "Content-Language", "Content-Length", |
| | 1 | 21 | | "Content-Location", "Content-MD5", "Content-Range", "Content-Security-Policy", "Content-Type", |
| | 1 | 22 | | "Cookie", "Cookie2", "Date", "ETag", "Expect", "Expect-CT", "Expires", "From", "grpc-encoding", |
| | 1 | 23 | | "grpc-message", "grpc-status", "Host", "If-Match", "If-Modified-Since", "If-None-Match", |
| | 1 | 24 | | "If-Range", "If-Unmodified-Since", "Keep-Alive", "Last-Modified", "Link", "Location", |
| | 1 | 25 | | "Max-Forwards", "Origin", "P3P", "Pragma", "Proxy-Authenticate", "Proxy-Authorization", |
| | 1 | 26 | | "Proxy-Connection", "Proxy-Support", "Public-Key-Pins", "Range", "Referer", "Referrer-Policy", |
| | 1 | 27 | | "Refresh", "Retry-After", "Sec-WebSocket-Accept", "Sec-WebSocket-Extensions", "Sec-WebSocket-Key", |
| | 1 | 28 | | "Sec-WebSocket-Protocol", "Sec-WebSocket-Version", "Server", "Server-Timing", "Set-Cookie", |
| | 1 | 29 | | "Set-Cookie2", "Strict-Transport-Security", "TE", "TSV", "Trailer", "Transfer-Encoding", |
| | 1 | 30 | | "Upgrade", "Upgrade-Insecure-Requests", "User-Agent", "Vary", "Via", "WWW-Authenticate", |
| | 1 | 31 | | "Warning", "X-AspNet-Version", "X-Cache", "X-Content-Duration", "X-Content-Type-Options", |
| | 1 | 32 | | "X-Frame-Options", "X-MSEdge-Ref", "X-Powered-By", "X-Request-ID", "X-UA-Compatible", "X-XSS-Protection" |
| | 1 | 33 | | ]; |
| | | 34 | | |
| | 1 | 35 | | private static readonly HttpRequestHeaders s_requestHeaders = new HttpRequestMessage().Headers; |
| | 1 | 36 | | private static readonly HttpContentHeaders s_contentHeaders = new ByteArrayContent([]).Headers; |
| | 1 | 37 | | private static readonly HttpResponseHeaders s_responseHeaders = new HttpResponseMessage().Headers; |
| | | 38 | | |
| | | 39 | | public void FuzzTarget(ReadOnlySpan<byte> bytes) |
| | 4900 | 40 | | { |
| | 4900 | 41 | | if (bytes.Length < 2) |
| | 4 | 42 | | { |
| | 4 | 43 | | return; |
| | | 44 | | } |
| | | 45 | | |
| | | 46 | | // We use the first byte to select a known header name. |
| | | 47 | | // The rest of the input is used as the UTF-16 header value. |
| | | 48 | | // The second byte is skipped to keep the value chars 2-byte aligned. |
| | 4896 | 49 | | string name = s_knownHeaderNames[bytes[0] % s_knownHeaderNames.Length]; |
| | 4896 | 50 | | string value = MemoryMarshal.Cast<byte, char>(bytes.Slice(2)).ToString(); |
| | | 51 | | |
| | 4896 | 52 | | Test(s_requestHeaders, name, value); |
| | 4896 | 53 | | Test(s_contentHeaders, name, value); |
| | 4896 | 54 | | Test(s_responseHeaders, name, value); |
| | | 55 | | |
| | | 56 | | static void Test(HttpHeaders headers, string name, string value) |
| | 14688 | 57 | | { |
| | 14688 | 58 | | headers.Clear(); |
| | | 59 | | |
| | 14688 | 60 | | if (headers.TryAddWithoutValidation(name, value)) |
| | 9865 | 61 | | { |
| | | 62 | | // Enumerating the header collection should never throw, |
| | | 63 | | // even if invalid values were added without validation. |
| | 69055 | 64 | | foreach (var _ in headers) { } |
| | | 65 | | |
| | 9865 | 66 | | headers.Clear(); |
| | | 67 | | |
| | | 68 | | // If the value is invalid, we should only throw FormatException. |
| | | 69 | | try |
| | 9865 | 70 | | { |
| | 9865 | 71 | | headers.Add(name, value); |
| | 5701 | 72 | | headers.Add(name, value); |
| | 5627 | 73 | | } |
| | 12714 | 74 | | catch (FormatException) { } |
| | | 75 | | |
| | | 76 | | // If values were added with validation, they should not contain CR or LF characters. |
| | 40771 | 77 | | foreach ((_, HeaderStringValues values) in headers.NonValidated) |
| | 5588 | 78 | | { |
| | 148728 | 79 | | foreach (string headerValue in values) |
| | 65982 | 80 | | { |
| | 65982 | 81 | | Assert.False(headerValue.ContainsAny('\r', '\n', '\0')); |
| | 65982 | 82 | | } |
| | 5588 | 83 | | } |
| | | 84 | | |
| | 40771 | 85 | | foreach ((_, IEnumerable<string> values) in headers) |
| | 5588 | 86 | | { |
| | 148728 | 87 | | foreach (string headerValue in values) |
| | 65982 | 88 | | { |
| | 65982 | 89 | | Assert.False(headerValue.ContainsAny('\r', '\n', '\0')); |
| | 65982 | 90 | | } |
| | 5588 | 91 | | } |
| | 9865 | 92 | | } |
| | 14688 | 93 | | } |
| | 4900 | 94 | | } |
| | | 95 | | } |