Skip to content

Commit 4a7e556

Browse files
Add support for setting security attributes on Http.Sys RequestQueue (#61325)
1 parent b212eb9 commit 4a7e556

File tree

7 files changed

+204
-56
lines changed

7 files changed

+204
-56
lines changed

src/Servers/HttpSys/src/HttpSysListener.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory)
7373
try
7474
{
7575
_serverSession = new ServerSession();
76-
_requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, Logger);
76+
_requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode,
77+
options.RequestQueueSecurityDescriptor, Logger);
7778
_urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger);
7879

7980
_disconnectListener = new DisconnectListener(_requestQueue, Logger);

src/Servers/HttpSys/src/HttpSysOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Globalization;
5+
using System.Security.AccessControl;
56
using System.Text;
67
using Microsoft.AspNetCore.Http.Features;
78

@@ -167,6 +168,14 @@ public long RequestQueueLimit
167168
}
168169
}
169170

171+
/// <summary>
172+
/// Gets or sets the security descriptor for the request queue.
173+
/// </summary>
174+
/// <remarks>
175+
/// Only applies when creating a new request queue, see <see cref="RequestQueueMode" />.
176+
/// </remarks>
177+
public GenericSecurityDescriptor? RequestQueueSecurityDescriptor { get; set; }
178+
170179
/// <summary>
171180
/// Gets or sets the maximum allowed size of any request body in bytes.
172181
/// When set to null, the maximum request body size is unlimited.

src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs

Lines changed: 91 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
using System.Diagnostics;
55
using System.Runtime.InteropServices;
6+
using System.Security.AccessControl;
67
using Microsoft.Extensions.Logging;
78
using Windows.Win32;
89
using Windows.Win32.Networking.HttpServer;
10+
using Windows.Win32.Security;
911

1012
namespace Microsoft.AspNetCore.Server.HttpSys;
1113

@@ -16,82 +18,116 @@ internal sealed partial class RequestQueue
1618
private bool _disposed;
1719

1820
internal RequestQueue(string requestQueueName, ILogger logger)
19-
: this(requestQueueName, RequestQueueMode.Attach, logger, receiver: true)
21+
: this(requestQueueName, RequestQueueMode.Attach, securityDescriptor: null, logger, receiver: true)
2022
{
2123
}
2224

23-
internal RequestQueue(string? requestQueueName, RequestQueueMode mode, ILogger logger)
24-
: this(requestQueueName, mode, logger, false)
25+
internal RequestQueue(string? requestQueueName, RequestQueueMode mode, GenericSecurityDescriptor? securityDescriptor, ILogger logger)
26+
: this(requestQueueName, mode, securityDescriptor, logger, false)
2527
{ }
2628

27-
private RequestQueue(string? requestQueueName, RequestQueueMode mode, ILogger logger, bool receiver)
29+
private RequestQueue(string? requestQueueName, RequestQueueMode mode, GenericSecurityDescriptor? securityDescriptor, ILogger logger, bool receiver)
2830
{
2931
_mode = mode;
3032
_logger = logger;
3133

3234
var flags = 0u;
3335
Created = true;
3436

35-
if (_mode == RequestQueueMode.Attach)
37+
SECURITY_ATTRIBUTES? securityAttributes = null;
38+
nint? pSecurityDescriptor = null;
39+
40+
try
3641
{
37-
flags = PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING;
38-
Created = false;
39-
if (receiver)
42+
if (_mode == RequestQueueMode.Attach)
4043
{
41-
flags |= PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_DELEGATION;
44+
flags = PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING;
45+
Created = false;
46+
if (receiver)
47+
{
48+
flags |= PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_DELEGATION;
49+
}
50+
}
51+
else if (securityDescriptor is not null) // Create or CreateOrAttach
52+
{
53+
// Convert the security descriptor to a byte array
54+
byte[] securityDescriptorBytes = new byte[securityDescriptor.BinaryLength];
55+
securityDescriptor.GetBinaryForm(securityDescriptorBytes, 0);
56+
57+
// Allocate native memory for the security descriptor
58+
pSecurityDescriptor = Marshal.AllocHGlobal(securityDescriptorBytes.Length);
59+
Marshal.Copy(securityDescriptorBytes, 0, pSecurityDescriptor.Value, securityDescriptorBytes.Length);
60+
61+
unsafe
62+
{
63+
securityAttributes = new SECURITY_ATTRIBUTES
64+
{
65+
nLength = (uint)Marshal.SizeOf<SECURITY_ATTRIBUTES>(),
66+
lpSecurityDescriptor = pSecurityDescriptor.Value.ToPointer(),
67+
bInheritHandle = false
68+
};
69+
}
4270
}
43-
}
44-
45-
var statusCode = PInvoke.HttpCreateRequestQueue(
46-
HttpApi.Version,
47-
requestQueueName,
48-
default,
49-
flags,
50-
out var requestQueueHandle);
5171

52-
if (_mode == RequestQueueMode.CreateOrAttach && statusCode == ErrorCodes.ERROR_ALREADY_EXISTS)
53-
{
54-
// Tried to create, but it already exists so attach to it instead.
55-
Created = false;
56-
flags = PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING;
57-
statusCode = PInvoke.HttpCreateRequestQueue(
72+
var statusCode = PInvoke.HttpCreateRequestQueue(
5873
HttpApi.Version,
5974
requestQueueName,
60-
default,
75+
securityAttributes,
6176
flags,
62-
out requestQueueHandle);
63-
}
77+
out var requestQueueHandle);
6478

65-
if ((flags & PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING) != 0 && statusCode == ErrorCodes.ERROR_FILE_NOT_FOUND)
66-
{
67-
throw new HttpSysException((int)statusCode, $"Failed to attach to the given request queue '{requestQueueName}', the queue could not be found.");
68-
}
69-
else if (statusCode == ErrorCodes.ERROR_INVALID_NAME)
70-
{
71-
throw new HttpSysException((int)statusCode, $"The given request queue name '{requestQueueName}' is invalid.");
72-
}
73-
else if (statusCode != ErrorCodes.ERROR_SUCCESS)
74-
{
75-
throw new HttpSysException((int)statusCode);
76-
}
79+
if (_mode == RequestQueueMode.CreateOrAttach && statusCode == ErrorCodes.ERROR_ALREADY_EXISTS)
80+
{
81+
// Tried to create, but it already exists so attach to it instead.
82+
Created = false;
83+
flags = PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING;
84+
statusCode = PInvoke.HttpCreateRequestQueue(
85+
HttpApi.Version,
86+
requestQueueName,
87+
SecurityAttributes: default, // Attaching should not pass any security attributes
88+
flags,
89+
out requestQueueHandle);
90+
}
7791

78-
// Disabling callbacks when IO operation completes synchronously (returns ErrorCodes.ERROR_SUCCESS)
79-
if (HttpSysListener.SkipIOCPCallbackOnSuccess &&
80-
!PInvoke.SetFileCompletionNotificationModes(
81-
requestQueueHandle,
82-
(byte)(PInvoke.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS |
83-
PInvoke.FILE_SKIP_SET_EVENT_ON_HANDLE)))
84-
{
85-
requestQueueHandle.Dispose();
86-
throw new HttpSysException(Marshal.GetLastWin32Error());
87-
}
92+
if ((flags & PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING) != 0 && statusCode == ErrorCodes.ERROR_FILE_NOT_FOUND)
93+
{
94+
throw new HttpSysException((int)statusCode, $"Failed to attach to the given request queue '{requestQueueName}', the queue could not be found.");
95+
}
96+
else if (statusCode == ErrorCodes.ERROR_INVALID_NAME)
97+
{
98+
throw new HttpSysException((int)statusCode, $"The given request queue name '{requestQueueName}' is invalid.");
99+
}
100+
else if (statusCode != ErrorCodes.ERROR_SUCCESS)
101+
{
102+
throw new HttpSysException((int)statusCode);
103+
}
104+
105+
// Disabling callbacks when IO operation completes synchronously (returns ErrorCodes.ERROR_SUCCESS)
106+
if (HttpSysListener.SkipIOCPCallbackOnSuccess &&
107+
!PInvoke.SetFileCompletionNotificationModes(
108+
requestQueueHandle,
109+
(byte)(PInvoke.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS |
110+
PInvoke.FILE_SKIP_SET_EVENT_ON_HANDLE)))
111+
{
112+
requestQueueHandle.Dispose();
113+
throw new HttpSysException(Marshal.GetLastWin32Error());
114+
}
88115

89-
Handle = requestQueueHandle;
90-
BoundHandle = ThreadPoolBoundHandle.BindHandle(Handle);
116+
Handle = requestQueueHandle;
117+
BoundHandle = ThreadPoolBoundHandle.BindHandle(Handle);
91118

92-
if (!Created)
119+
if (!Created)
120+
{
121+
Log.AttachedToQueue(_logger, requestQueueName);
122+
}
123+
}
124+
finally
93125
{
94-
Log.AttachedToQueue(_logger, requestQueueName);
126+
if (pSecurityDescriptor is not null)
127+
{
128+
// Free the allocated memory for the security descriptor
129+
Marshal.FreeHGlobal(pSecurityDescriptor.Value);
130+
}
95131
}
96132
}
97133

@@ -143,6 +179,9 @@ public void Dispose()
143179
}
144180

145181
_disposed = true;
182+
183+
PInvoke.HttpCloseRequestQueue(Handle);
184+
146185
BoundHandle.Dispose();
147186
Handle.Dispose();
148187
}

src/Servers/HttpSys/src/NativeMethods.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ HTTPAPI_VERSION
4242
HttpCancelHttpRequest
4343
HttpCloseServerSession
4444
HttpCloseUrlGroup
45+
HttpCloseRequestQueue
4546
HttpCreateRequestQueue
4647
HttpCreateServerSession
4748
HttpCreateUrlGroup
@@ -59,3 +60,6 @@ HttpSetUrlGroupProperty
5960
SetFileCompletionNotificationModes
6061
SOCKADDR_IN
6162
SOCKADDR_IN6
63+
GetSecurityInfo
64+
GetSecurityDescriptorLength
65+
LocalFree
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
#nullable enable
22
Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.get -> System.Action<Microsoft.AspNetCore.Http.Features.IFeatureCollection!, System.ReadOnlySpan<byte>>?
33
Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.set -> void
4+
Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.RequestQueueSecurityDescriptor.get -> System.Security.AccessControl.GenericSecurityDescriptor?
5+
Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.RequestQueueSecurityDescriptor.set -> void

src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33

44
using System.Net.Http;
55
using System.Runtime.InteropServices;
6+
using System.Security.AccessControl;
7+
using System.Security.Principal;
68
using Microsoft.AspNetCore.Hosting;
79
using Microsoft.AspNetCore.Hosting.Server;
810
using Microsoft.AspNetCore.Http;
9-
using Microsoft.AspNetCore.HttpSys.Internal;
1011
using Microsoft.AspNetCore.InternalTesting;
1112
using Microsoft.Extensions.DependencyInjection;
1213
using Microsoft.Extensions.Hosting;
14+
using Windows.Win32;
15+
using Windows.Win32.Foundation;
16+
using Windows.Win32.Security;
1317

1418
namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests;
1519

@@ -266,6 +270,95 @@ public async Task DelegateAfterReceiverRestart()
266270
destination?.Dispose();
267271
}
268272

273+
[ConditionalFact]
274+
[DelegateSupportedCondition(true)]
275+
public async Task DelegateRequestTestCanSetSecurityDescriptor()
276+
{
277+
// Create a new security descriptor
278+
CommonSecurityDescriptor securityDescriptor = new CommonSecurityDescriptor(false, false, string.Empty);
279+
280+
// Create a discretionary access control list (DACL)
281+
DiscretionaryAcl dacl = new DiscretionaryAcl(false, false, 2);
282+
dacl.AddAccess(AccessControlType.Allow, new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), -1, InheritanceFlags.None, PropagationFlags.None);
283+
dacl.AddAccess(AccessControlType.Deny, new SecurityIdentifier(WellKnownSidType.BuiltinGuestsSid, null), -1, InheritanceFlags.None, PropagationFlags.None);
284+
285+
// Assign the DACL to the security descriptor
286+
securityDescriptor.DiscretionaryAcl = dacl;
287+
288+
var queueName = Guid.NewGuid().ToString();
289+
using var receiver = Utilities.CreateHttpServer(out var receiverAddress, async httpContext =>
290+
{
291+
await httpContext.Response.WriteAsync(_expectedResponseString);
292+
},
293+
options =>
294+
{
295+
options.RequestQueueName = queueName;
296+
options.RequestQueueSecurityDescriptor = securityDescriptor;
297+
}, LoggerFactory);
298+
299+
DelegationRule destination = default;
300+
301+
using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, httpContext =>
302+
{
303+
var delegateFeature = httpContext.Features.Get<IHttpSysRequestDelegationFeature>();
304+
delegateFeature.DelegateRequest(destination);
305+
return Task.CompletedTask;
306+
}, LoggerFactory);
307+
308+
var delegationProperty = delegator.Features.Get<IServerDelegationFeature>();
309+
destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress);
310+
311+
AssertPermissions(destination.Queue.Handle);
312+
unsafe void AssertPermissions(SafeHandle handle)
313+
{
314+
PSECURITY_DESCRIPTOR pSecurityDescriptor = new();
315+
316+
WIN32_ERROR result = PInvoke.GetSecurityInfo(
317+
handle,
318+
Windows.Win32.Security.Authorization.SE_OBJECT_TYPE.SE_KERNEL_OBJECT,
319+
4, // DACL_SECURITY_INFORMATION
320+
null,
321+
null,
322+
null,
323+
null,
324+
&pSecurityDescriptor);
325+
326+
var length = (int)PInvoke.GetSecurityDescriptorLength(pSecurityDescriptor);
327+
328+
// Copy the security descriptor to a managed byte array
329+
byte[] securityDescriptorBytes = new byte[length];
330+
Marshal.Copy(new IntPtr(pSecurityDescriptor.Value), securityDescriptorBytes, 0, length);
331+
332+
// Convert the byte array to a RawSecurityDescriptor
333+
var securityDescriptor = new RawSecurityDescriptor(securityDescriptorBytes, 0);
334+
335+
var checkedAllowUser = false;
336+
var checkedDenyGuest = false;
337+
338+
foreach (CommonAce ace in securityDescriptor.DiscretionaryAcl)
339+
{
340+
if (ace.SecurityIdentifier.IsWellKnown(WellKnownSidType.BuiltinGuestsSid))
341+
{
342+
Assert.Equal(AceType.AccessDenied, ace.AceType);
343+
checkedDenyGuest = true;
344+
}
345+
else if (ace.SecurityIdentifier.IsWellKnown(WellKnownSidType.BuiltinUsersSid))
346+
{
347+
Assert.Equal(AceType.AccessAllowed, ace.AceType);
348+
checkedAllowUser = true;
349+
}
350+
}
351+
352+
PInvoke.LocalFree((HLOCAL)pSecurityDescriptor.Value);
353+
354+
Assert.True(checkedDenyGuest && checkedAllowUser, "DACL does not contain the expected ACEs");
355+
}
356+
357+
var responseString = await SendRequestAsync(delegatorAddress);
358+
Assert.Equal(_expectedResponseString, responseString);
359+
destination?.Dispose();
360+
}
361+
269362
private async Task<string> SendRequestAsync(string uri)
270363
{
271364
using var client = new HttpClient();

src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@@ -20,7 +20,7 @@
2020
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" LinkBase="Shared\" />
2121
<Compile Remove="$(SharedSourceRoot)ServerInfrastructure\DuplexPipe.cs" />
2222
<Compile Remove="$(SharedSourceRoot)ServerInfrastructure\StringUtilities.cs" />
23-
<Compile Include="$(SharedSourceRoot)InternalHeaderNames.cs" Linkbase="shared"/>
23+
<Compile Include="$(SharedSourceRoot)InternalHeaderNames.cs" Linkbase="shared" />
2424
<Compile Include="$(SharedSourceRoot)TestResources.cs" LinkBase="shared" />
2525
<Content Include="$(SharedSourceRoot)TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
2626
<Compile Include="$(SharedSourceRoot)TransportTestHelpers\MsQuicSupportedAttribute.cs" LinkBase="shared\" />

0 commit comments

Comments
 (0)