From 0a3afc9b17542e494a3da1438ab57dfd311d5caa Mon Sep 17 00:00:00 2001 From: Mingzhe Huang Date: Wed, 13 Apr 2022 00:01:08 +0800 Subject: [PATCH] Clientruntime/sanitize request header (#28169) * fix(ClientRuntime): sanitize request headers 1. add `HttpRequestSanitizer` to sanitize headers 2. update `HttpRequestMessageWrapper` to sanitive headers during headers copy 3. add test cases * prepare release notes of 2.3.24 --- .../HttpRequestMessageWrapper.cs | 2 + .../Microsoft.Rest.ClientRuntime.csproj | 5 +- .../Utilities/HttpRequestSanitizer.cs | 65 +++++++++++++ .../HttpRequestSanitizerTest.cs | 96 +++++++++++++++++++ .../ServiceClientTests.cs | 2 +- 5 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 sdk/mgmtcommon/ClientRuntime/ClientRuntime/Utilities/HttpRequestSanitizer.cs create mode 100644 sdk/mgmtcommon/ClientRuntime/Tests/ClientRuntime.NetCore.Tests/HttpRequestSanitizerTest.cs diff --git a/sdk/mgmtcommon/ClientRuntime/ClientRuntime/HttpRequestMessageWrapper.cs b/sdk/mgmtcommon/ClientRuntime/ClientRuntime/HttpRequestMessageWrapper.cs index 8252d364a67d3..3cee7645c2f3a 100644 --- a/sdk/mgmtcommon/ClientRuntime/ClientRuntime/HttpRequestMessageWrapper.cs +++ b/sdk/mgmtcommon/ClientRuntime/ClientRuntime/HttpRequestMessageWrapper.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using Microsoft.Rest.Utilities; using System; using System.Collections.Generic; using System.Net.Http; @@ -26,6 +27,7 @@ public HttpRequestMessageWrapper(HttpRequestMessage httpRequest, string content) this.CopyHeaders(httpRequest.Headers); this.CopyHeaders(httpRequest.GetContentHeaders()); + HttpRequestSanitizer.SanitizerHeaders(Headers); this.Content = content; this.Method = httpRequest.Method; diff --git a/sdk/mgmtcommon/ClientRuntime/ClientRuntime/Microsoft.Rest.ClientRuntime.csproj b/sdk/mgmtcommon/ClientRuntime/ClientRuntime/Microsoft.Rest.ClientRuntime.csproj index 724c69b38be58..3c97e78233fb1 100644 --- a/sdk/mgmtcommon/ClientRuntime/ClientRuntime/Microsoft.Rest.ClientRuntime.csproj +++ b/sdk/mgmtcommon/ClientRuntime/ClientRuntime/Microsoft.Rest.ClientRuntime.csproj @@ -3,12 +3,13 @@ Infrastructure for error handling, tracing, and HttpClient pipeline configuration. Required by client libraries generated using AutoRest. Microsoft.Rest.ClientRuntime Client Runtime Library for Microsoft AutoRest Generated Clients - 2.3.23 + 2.3.24 Microsoft.Rest.ClientRuntime Microsoft AutoRest ClientRuntime $(NugetCommonTags) $(NugetCommonProfileTags) diff --git a/sdk/mgmtcommon/ClientRuntime/ClientRuntime/Utilities/HttpRequestSanitizer.cs b/sdk/mgmtcommon/ClientRuntime/ClientRuntime/Utilities/HttpRequestSanitizer.cs new file mode 100644 index 0000000000000..2e4b4ba5fd963 --- /dev/null +++ b/sdk/mgmtcommon/ClientRuntime/ClientRuntime/Utilities/HttpRequestSanitizer.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Rest.Utilities +{ + /// + /// Sanitizer used internall by . + /// + internal class HttpRequestSanitizer + { + private readonly static string _redactedPlaceholder = "REDACTED"; + private readonly static HashSet _allowedHeaders = new HashSet(new string[] + { + "x-ms-request-id", + "x-ms-client-request-id", + "x-ms-return-client-request-id", + "traceparent", + "MS-CV", + + "Accept", + "Cache-Control", + "Connection", + "Content-Length", + "Content-Type", + "Date", + "ETag", + "Expires", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Unmodified-Since", + "Last-Modified", + "Pragma", + "Request-Id", + "Retry-After", + "Server", + "Transfer-Encoding", + "User-Agent", + "WWW-Authenticate" // OAuth Challenge header. + }, StringComparer.OrdinalIgnoreCase); + + /// + /// Sanitize value of sensitive headers in the given . + /// + /// A collection of headers to sanitize. + public static void SanitizerHeaders(IDictionary> headers) + { + if (headers == null) + { + return; + } + + var namesOfHeaderToSanitize = headers.Keys.Except(_allowedHeaders, StringComparer.OrdinalIgnoreCase).ToList(); + + foreach (string name in namesOfHeaderToSanitize) + { + headers[name] = new string[] { _redactedPlaceholder }; + } + } + } +} \ No newline at end of file diff --git a/sdk/mgmtcommon/ClientRuntime/Tests/ClientRuntime.NetCore.Tests/HttpRequestSanitizerTest.cs b/sdk/mgmtcommon/ClientRuntime/Tests/ClientRuntime.NetCore.Tests/HttpRequestSanitizerTest.cs new file mode 100644 index 0000000000000..af7c7ea947a77 --- /dev/null +++ b/sdk/mgmtcommon/ClientRuntime/Tests/ClientRuntime.NetCore.Tests/HttpRequestSanitizerTest.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.Rest.ClientRuntime.Tests +{ + using System.Net; + using System.Net.Http; + using System.Collections.Generic; + using Xunit; + using System.Linq; + + public class HttpRequestSanitizerTest + { + [Theory] + [InlineData("authorization")] + [InlineData("Authorization")] + [InlineData("AUTHORIZATION")] + public void SanitizeAuthorizationHeader(string headerName) + { + var request = CreateRequestWrapper(headerName, "test"); + + Assert.True(request.Headers.TryGetValue(HttpRequestHeader.Authorization.ToString(), out IEnumerable sanitizedValues)); + Assert.Single(sanitizedValues); + Assert.NotEqual("test", sanitizedValues.First()); + Assert.Equal("REDACTED", sanitizedValues.First()); + } + + [Theory] + [InlineData("custom")] + [InlineData("foo")] + [InlineData("x-ms-secret")] + public void SanitizeCustomHeader(string headerName) + { + var request = CreateRequestWrapper(headerName, "test"); + + Assert.True(request.Headers.TryGetValue(headerName, out IEnumerable sanitizedValues)); + Assert.Single(sanitizedValues); + Assert.NotEqual("test", sanitizedValues.First()); + Assert.Equal("REDACTED", sanitizedValues.First()); + } + + [Theory] + [InlineData("Accept", "application/json")] + [InlineData("User-Agent", "azure-sdk")] + [InlineData("Pragma", "foo")] + public void KeepAllowedHeaders(string headerName, string headerValue) + { + var request = CreateRequestWrapper(headerName, headerValue); + + Assert.True(request.Headers.TryGetValue(headerName, out IEnumerable sanitizedValues)); + Assert.Single(sanitizedValues); + Assert.Equal(headerValue, sanitizedValues.First()); + } + + [Theory] + [InlineData("accept", "Accept", "application/json")] + [InlineData("user-agent", "User-Agent", "azure-sdk")] + [InlineData("pragma", "Pragma", "foo")] + public void AllowedHeaderNamesAreCaseInsensitive(string headerName, string standardName, string headerValue) + { + var request = CreateRequestWrapper(headerName, headerValue); + + Assert.True(request.Headers.TryGetValue(standardName, out IEnumerable sanitizedValues)); + Assert.Single(sanitizedValues); + Assert.Equal(headerValue, sanitizedValues.First()); + } + + [Fact] + public void OnlySanitizeNotAllowedHeader() + { + var request = CreateRequestWrapper("Authorization", "test"); + request.Headers.Add("Pragma", new string[] { "foo" }); + request.Headers.Add("User-Agent", new string[] { "azure-sdk" }); + + Assert.True(request.Headers.TryGetValue(HttpRequestHeader.Authorization.ToString(), out IEnumerable sanitizedValues)); + Assert.Single(sanitizedValues); + Assert.NotEqual("test", sanitizedValues.First()); + Assert.Equal("REDACTED", sanitizedValues.First()); + + Assert.True(request.Headers.TryGetValue("Pragma", out sanitizedValues)); + Assert.Single(sanitizedValues); + Assert.Equal("foo", sanitizedValues.First()); + + Assert.True(request.Headers.TryGetValue("User-Agent", out sanitizedValues)); + Assert.Single(sanitizedValues); + Assert.Equal("azure-sdk", sanitizedValues.First()); + } + + private HttpRequestMessageWrapper CreateRequestWrapper(string headerName, string headerValue) + { + var request = new HttpRequestMessage(); + request.Headers.Add(headerName, headerValue); + return new HttpRequestMessageWrapper(request, ""); + } + } +} \ No newline at end of file diff --git a/sdk/mgmtcommon/ClientRuntime/Tests/ClientRuntime.NetCore.Tests/ServiceClientTests.cs b/sdk/mgmtcommon/ClientRuntime/Tests/ClientRuntime.NetCore.Tests/ServiceClientTests.cs index 2a0c26466ba86..3f2964aa03a0d 100644 --- a/sdk/mgmtcommon/ClientRuntime/Tests/ClientRuntime.NetCore.Tests/ServiceClientTests.cs +++ b/sdk/mgmtcommon/ClientRuntime/Tests/ClientRuntime.NetCore.Tests/ServiceClientTests.cs @@ -206,7 +206,7 @@ public void HeadersAndPayloadAreNotDisposed() GC.Collect(); Assert.NotNull(ex.Request); Assert.NotNull(ex.Response); - Assert.Equal("2013-11-01", ex.Request.Headers["x-ms-version"].First()); + Assert.Equal("REDACTED", ex.Request.Headers["x-ms-version"].First()); Assert.Equal("Text", ex.Response.Content); } }