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 @@
-
+