diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 0f168e53bd2f..02f595c98fe6 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -73,7 +73,8 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) try { _serverSession = new ServerSession(); - _requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, Logger); + _requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, + options.RequestQueueSecurityDescriptor, Logger); _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); _disconnectListener = new DisconnectListener(_requestQueue, Logger); diff --git a/src/Servers/HttpSys/src/HttpSysOptions.cs b/src/Servers/HttpSys/src/HttpSysOptions.cs index 3e83e10212f9..bb9cdc6954e4 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Security.AccessControl; using System.Text; using Microsoft.AspNetCore.Http.Features; @@ -167,6 +168,14 @@ public long RequestQueueLimit } } + /// + /// Gets or sets the security descriptor for the request queue. + /// + /// + /// Only applies when creating a new request queue, see . + /// + public GenericSecurityDescriptor? RequestQueueSecurityDescriptor { get; set; } + /// /// Gets or sets the maximum allowed size of any request body in bytes. /// When set to null, the maximum request body size is unlimited. diff --git a/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs b/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs index 810bdcce3c57..a2e86245de75 100644 --- a/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs +++ b/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs @@ -3,9 +3,11 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using System.Security.AccessControl; using Microsoft.Extensions.Logging; using Windows.Win32; using Windows.Win32.Networking.HttpServer; +using Windows.Win32.Security; namespace Microsoft.AspNetCore.Server.HttpSys; @@ -16,15 +18,15 @@ internal sealed partial class RequestQueue private bool _disposed; internal RequestQueue(string requestQueueName, ILogger logger) - : this(requestQueueName, RequestQueueMode.Attach, logger, receiver: true) + : this(requestQueueName, RequestQueueMode.Attach, securityDescriptor: null, logger, receiver: true) { } - internal RequestQueue(string? requestQueueName, RequestQueueMode mode, ILogger logger) - : this(requestQueueName, mode, logger, false) + internal RequestQueue(string? requestQueueName, RequestQueueMode mode, GenericSecurityDescriptor? securityDescriptor, ILogger logger) + : this(requestQueueName, mode, securityDescriptor, logger, false) { } - private RequestQueue(string? requestQueueName, RequestQueueMode mode, ILogger logger, bool receiver) + private RequestQueue(string? requestQueueName, RequestQueueMode mode, GenericSecurityDescriptor? securityDescriptor, ILogger logger, bool receiver) { _mode = mode; _logger = logger; @@ -32,66 +34,100 @@ private RequestQueue(string? requestQueueName, RequestQueueMode mode, ILogger lo var flags = 0u; Created = true; - if (_mode == RequestQueueMode.Attach) + SECURITY_ATTRIBUTES? securityAttributes = null; + nint? pSecurityDescriptor = null; + + try { - flags = PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING; - Created = false; - if (receiver) + if (_mode == RequestQueueMode.Attach) { - flags |= PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_DELEGATION; + flags = PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING; + Created = false; + if (receiver) + { + flags |= PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_DELEGATION; + } + } + else if (securityDescriptor is not null) // Create or CreateOrAttach + { + // Convert the security descriptor to a byte array + byte[] securityDescriptorBytes = new byte[securityDescriptor.BinaryLength]; + securityDescriptor.GetBinaryForm(securityDescriptorBytes, 0); + + // Allocate native memory for the security descriptor + pSecurityDescriptor = Marshal.AllocHGlobal(securityDescriptorBytes.Length); + Marshal.Copy(securityDescriptorBytes, 0, pSecurityDescriptor.Value, securityDescriptorBytes.Length); + + unsafe + { + securityAttributes = new SECURITY_ATTRIBUTES + { + nLength = (uint)Marshal.SizeOf(), + lpSecurityDescriptor = pSecurityDescriptor.Value.ToPointer(), + bInheritHandle = false + }; + } } - } - - var statusCode = PInvoke.HttpCreateRequestQueue( - HttpApi.Version, - requestQueueName, - default, - flags, - out var requestQueueHandle); - if (_mode == RequestQueueMode.CreateOrAttach && statusCode == ErrorCodes.ERROR_ALREADY_EXISTS) - { - // Tried to create, but it already exists so attach to it instead. - Created = false; - flags = PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING; - statusCode = PInvoke.HttpCreateRequestQueue( + var statusCode = PInvoke.HttpCreateRequestQueue( HttpApi.Version, requestQueueName, - default, + securityAttributes, flags, - out requestQueueHandle); - } + out var requestQueueHandle); - if ((flags & PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING) != 0 && statusCode == ErrorCodes.ERROR_FILE_NOT_FOUND) - { - throw new HttpSysException((int)statusCode, $"Failed to attach to the given request queue '{requestQueueName}', the queue could not be found."); - } - else if (statusCode == ErrorCodes.ERROR_INVALID_NAME) - { - throw new HttpSysException((int)statusCode, $"The given request queue name '{requestQueueName}' is invalid."); - } - else if (statusCode != ErrorCodes.ERROR_SUCCESS) - { - throw new HttpSysException((int)statusCode); - } + if (_mode == RequestQueueMode.CreateOrAttach && statusCode == ErrorCodes.ERROR_ALREADY_EXISTS) + { + // Tried to create, but it already exists so attach to it instead. + Created = false; + flags = PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING; + statusCode = PInvoke.HttpCreateRequestQueue( + HttpApi.Version, + requestQueueName, + SecurityAttributes: default, // Attaching should not pass any security attributes + flags, + out requestQueueHandle); + } - // Disabling callbacks when IO operation completes synchronously (returns ErrorCodes.ERROR_SUCCESS) - if (HttpSysListener.SkipIOCPCallbackOnSuccess && - !PInvoke.SetFileCompletionNotificationModes( - requestQueueHandle, - (byte)(PInvoke.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS | - PInvoke.FILE_SKIP_SET_EVENT_ON_HANDLE))) - { - requestQueueHandle.Dispose(); - throw new HttpSysException(Marshal.GetLastWin32Error()); - } + if ((flags & PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING) != 0 && statusCode == ErrorCodes.ERROR_FILE_NOT_FOUND) + { + throw new HttpSysException((int)statusCode, $"Failed to attach to the given request queue '{requestQueueName}', the queue could not be found."); + } + else if (statusCode == ErrorCodes.ERROR_INVALID_NAME) + { + throw new HttpSysException((int)statusCode, $"The given request queue name '{requestQueueName}' is invalid."); + } + else if (statusCode != ErrorCodes.ERROR_SUCCESS) + { + throw new HttpSysException((int)statusCode); + } + + // Disabling callbacks when IO operation completes synchronously (returns ErrorCodes.ERROR_SUCCESS) + if (HttpSysListener.SkipIOCPCallbackOnSuccess && + !PInvoke.SetFileCompletionNotificationModes( + requestQueueHandle, + (byte)(PInvoke.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS | + PInvoke.FILE_SKIP_SET_EVENT_ON_HANDLE))) + { + requestQueueHandle.Dispose(); + throw new HttpSysException(Marshal.GetLastWin32Error()); + } - Handle = requestQueueHandle; - BoundHandle = ThreadPoolBoundHandle.BindHandle(Handle); + Handle = requestQueueHandle; + BoundHandle = ThreadPoolBoundHandle.BindHandle(Handle); - if (!Created) + if (!Created) + { + Log.AttachedToQueue(_logger, requestQueueName); + } + } + finally { - Log.AttachedToQueue(_logger, requestQueueName); + if (pSecurityDescriptor is not null) + { + // Free the allocated memory for the security descriptor + Marshal.FreeHGlobal(pSecurityDescriptor.Value); + } } } @@ -143,6 +179,9 @@ public void Dispose() } _disposed = true; + + PInvoke.HttpCloseRequestQueue(Handle); + BoundHandle.Dispose(); Handle.Dispose(); } diff --git a/src/Servers/HttpSys/src/NativeMethods.txt b/src/Servers/HttpSys/src/NativeMethods.txt index 49222f530b01..0ad43a083c5d 100644 --- a/src/Servers/HttpSys/src/NativeMethods.txt +++ b/src/Servers/HttpSys/src/NativeMethods.txt @@ -42,6 +42,7 @@ HTTPAPI_VERSION HttpCancelHttpRequest HttpCloseServerSession HttpCloseUrlGroup +HttpCloseRequestQueue HttpCreateRequestQueue HttpCreateServerSession HttpCreateUrlGroup @@ -59,3 +60,6 @@ HttpSetUrlGroupProperty SetFileCompletionNotificationModes SOCKADDR_IN SOCKADDR_IN6 +GetSecurityInfo +GetSecurityDescriptorLength +LocalFree diff --git a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt index e18d576e45d3..393f7b26a8f3 100644 --- a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt +++ b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.get -> System.Action>? Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.set -> void +Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.RequestQueueSecurityDescriptor.get -> System.Security.AccessControl.GenericSecurityDescriptor? +Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.RequestQueueSecurityDescriptor.set -> void diff --git a/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs b/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs index 8cb6332a8f6d..daa5dd7d466c 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs @@ -3,13 +3,17 @@ using System.Net.Http; using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Security.Principal; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Security; namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests; @@ -266,6 +270,95 @@ public async Task DelegateAfterReceiverRestart() destination?.Dispose(); } + [ConditionalFact] + [DelegateSupportedCondition(true)] + public async Task DelegateRequestTestCanSetSecurityDescriptor() + { + // Create a new security descriptor + CommonSecurityDescriptor securityDescriptor = new CommonSecurityDescriptor(false, false, string.Empty); + + // Create a discretionary access control list (DACL) + DiscretionaryAcl dacl = new DiscretionaryAcl(false, false, 2); + dacl.AddAccess(AccessControlType.Allow, new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), -1, InheritanceFlags.None, PropagationFlags.None); + dacl.AddAccess(AccessControlType.Deny, new SecurityIdentifier(WellKnownSidType.BuiltinGuestsSid, null), -1, InheritanceFlags.None, PropagationFlags.None); + + // Assign the DACL to the security descriptor + securityDescriptor.DiscretionaryAcl = dacl; + + var queueName = Guid.NewGuid().ToString(); + using var receiver = Utilities.CreateHttpServer(out var receiverAddress, async httpContext => + { + await httpContext.Response.WriteAsync(_expectedResponseString); + }, + options => + { + options.RequestQueueName = queueName; + options.RequestQueueSecurityDescriptor = securityDescriptor; + }, LoggerFactory); + + DelegationRule destination = default; + + using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, httpContext => + { + var delegateFeature = httpContext.Features.Get(); + delegateFeature.DelegateRequest(destination); + return Task.CompletedTask; + }, LoggerFactory); + + var delegationProperty = delegator.Features.Get(); + destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress); + + AssertPermissions(destination.Queue.Handle); + unsafe void AssertPermissions(SafeHandle handle) + { + PSECURITY_DESCRIPTOR pSecurityDescriptor = new(); + + WIN32_ERROR result = PInvoke.GetSecurityInfo( + handle, + Windows.Win32.Security.Authorization.SE_OBJECT_TYPE.SE_KERNEL_OBJECT, + 4, // DACL_SECURITY_INFORMATION + null, + null, + null, + null, + &pSecurityDescriptor); + + var length = (int)PInvoke.GetSecurityDescriptorLength(pSecurityDescriptor); + + // Copy the security descriptor to a managed byte array + byte[] securityDescriptorBytes = new byte[length]; + Marshal.Copy(new IntPtr(pSecurityDescriptor.Value), securityDescriptorBytes, 0, length); + + // Convert the byte array to a RawSecurityDescriptor + var securityDescriptor = new RawSecurityDescriptor(securityDescriptorBytes, 0); + + var checkedAllowUser = false; + var checkedDenyGuest = false; + + foreach (CommonAce ace in securityDescriptor.DiscretionaryAcl) + { + if (ace.SecurityIdentifier.IsWellKnown(WellKnownSidType.BuiltinGuestsSid)) + { + Assert.Equal(AceType.AccessDenied, ace.AceType); + checkedDenyGuest = true; + } + else if (ace.SecurityIdentifier.IsWellKnown(WellKnownSidType.BuiltinUsersSid)) + { + Assert.Equal(AceType.AccessAllowed, ace.AceType); + checkedAllowUser = true; + } + } + + PInvoke.LocalFree((HLOCAL)pSecurityDescriptor.Value); + + Assert.True(checkedDenyGuest && checkedAllowUser, "DACL does not contain the expected ACEs"); + } + + var responseString = await SendRequestAsync(delegatorAddress); + Assert.Equal(_expectedResponseString, responseString); + destination?.Dispose(); + } + private async Task SendRequestAsync(string uri) { using var client = new HttpClient(); diff --git a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj index 56f300b89198..f59ba8ab7c22 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj +++ b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -20,7 +20,7 @@ - +