Skip to content

Commit aaaaf57

Browse files
authored
Implement MaxRequestBodySize feature for IIS inprocess (#9475)
1 parent 93d82b0 commit aaaaf57

33 files changed

+832
-35
lines changed

src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ConfigurationSource.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#define CS_WINDOWS_AUTHENTICATION_SECTION L"system.webServer/security/authentication/windowsAuthentication"
1414
#define CS_BASIC_AUTHENTICATION_SECTION L"system.webServer/security/authentication/basicAuthentication"
1515
#define CS_ANONYMOUS_AUTHENTICATION_SECTION L"system.webServer/security/authentication/anonymousAuthentication"
16+
#define CS_MAX_REQUEST_BODY_SIZE_SECTION L"system.webServer/security/requestFiltering"
1617

1718
class ConfigurationSource: NonCopyable
1819
{

src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ InProcessOptions::InProcessOptions(const ConfigurationSource &configurationSourc
4444
m_fWindowsAuthEnabled(false),
4545
m_fBasicAuthEnabled(false),
4646
m_fAnonymousAuthEnabled(false),
47+
m_dwMaxRequestBodySize(INFINITE),
4748
m_dwStartupTimeLimitInMS(INFINITE),
4849
m_dwShutdownTimeLimitInMS(INFINITE)
4950
{
@@ -71,6 +72,24 @@ InProcessOptions::InProcessOptions(const ConfigurationSource &configurationSourc
7172
const auto anonAuthSection = configurationSource.GetSection(CS_ANONYMOUS_AUTHENTICATION_SECTION);
7273
m_fAnonymousAuthEnabled = anonAuthSection && anonAuthSection->GetBool(CS_ENABLED).value_or(false);
7374

75+
const auto requestFilteringSection = configurationSource.GetSection(CS_MAX_REQUEST_BODY_SIZE_SECTION);
76+
if (requestFilteringSection != nullptr)
77+
{
78+
// The requestFiltering section is enabled by default in most scenarios. However, if the value
79+
// maxAllowedContentLength isn't set, it defaults to 30_000_000 in IIS.
80+
// The section element won't be defined if the feature is disabled, so the presence of the section tells
81+
// us whether there should be a default or not.
82+
auto requestLimitSection = requestFilteringSection->GetSection(L"requestLimits").value_or(nullptr);
83+
if (requestLimitSection != nullptr)
84+
{
85+
m_dwMaxRequestBodySize = requestLimitSection->GetLong(L"maxAllowedContentLength").value_or(30000000);
86+
}
87+
else
88+
{
89+
m_dwMaxRequestBodySize = 30000000;
90+
}
91+
}
92+
7493
if (pSite != nullptr)
7594
{
7695
m_bindingInformation = BindingInformation::Load(configurationSource, *pSite);

src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ class InProcessOptions: NonCopyable
9494
return m_dwShutdownTimeLimitInMS;
9595
}
9696

97+
DWORD
98+
QueryMaxRequestBodySizeLimit() const
99+
{
100+
return m_dwMaxRequestBodySize;
101+
}
102+
97103
const std::map<std::wstring, std::wstring, ignore_case_comparer>&
98104
QueryEnvironmentVariables() const
99105
{
@@ -128,6 +134,7 @@ class InProcessOptions: NonCopyable
128134
bool m_fAnonymousAuthEnabled;
129135
DWORD m_dwStartupTimeLimitInMS;
130136
DWORD m_dwShutdownTimeLimitInMS;
137+
DWORD m_dwMaxRequestBodySize;
131138
std::map<std::wstring, std::wstring, ignore_case_comparer> m_environmentVariables;
132139
std::vector<BindingInformation> m_bindingInformation;
133140

src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ IN_PROCESS_APPLICATION::ExecuteApplication()
252252
}
253253

254254
// Used to make .NET Runtime always log to event log when there is an unhandled exception.
255-
LOG_LAST_ERROR_IF(SetEnvironmentVariable(L"COMPlus_UseEntryPointFilter", L"1"));
255+
LOG_LAST_ERROR_IF(!SetEnvironmentVariable(L"COMPlus_UseEntryPointFilter", L"1"));
256256

257257
bool clrThreadExited;
258258
{

src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,13 @@ struct IISConfigurationData
191191
BOOL fBasicAuthEnabled;
192192
BOOL fAnonymousAuthEnable;
193193
BSTR pwzBindings;
194+
DWORD maxRequestBodySize;
194195
};
195196

196197
EXTERN_C __MIDL_DECLSPEC_DLLEXPORT
197198
HRESULT
198199
http_get_application_properties(
199-
_In_ IISConfigurationData* pIISCofigurationData
200+
_In_ IISConfigurationData* pIISConfigurationData
200201
)
201202
{
202203
auto pInProcessApplication = IN_PROCESS_APPLICATION::GetInstance();
@@ -207,15 +208,16 @@ http_get_application_properties(
207208

208209
const auto& pConfiguration = pInProcessApplication->QueryConfig();
209210

210-
pIISCofigurationData->pInProcessApplication = pInProcessApplication;
211-
pIISCofigurationData->pwzFullApplicationPath = SysAllocString(pInProcessApplication->QueryApplicationPhysicalPath().c_str());
212-
pIISCofigurationData->pwzVirtualApplicationPath = SysAllocString(pInProcessApplication->QueryApplicationVirtualPath().c_str());
213-
pIISCofigurationData->fWindowsAuthEnabled = pConfiguration.QueryWindowsAuthEnabled();
214-
pIISCofigurationData->fBasicAuthEnabled = pConfiguration.QueryBasicAuthEnabled();
215-
pIISCofigurationData->fAnonymousAuthEnable = pConfiguration.QueryAnonymousAuthEnabled();
211+
pIISConfigurationData->pInProcessApplication = pInProcessApplication;
212+
pIISConfigurationData->pwzFullApplicationPath = SysAllocString(pInProcessApplication->QueryApplicationPhysicalPath().c_str());
213+
pIISConfigurationData->pwzVirtualApplicationPath = SysAllocString(pInProcessApplication->QueryApplicationVirtualPath().c_str());
214+
pIISConfigurationData->fWindowsAuthEnabled = pConfiguration.QueryWindowsAuthEnabled();
215+
pIISConfigurationData->fBasicAuthEnabled = pConfiguration.QueryBasicAuthEnabled();
216+
pIISConfigurationData->fAnonymousAuthEnable = pConfiguration.QueryAnonymousAuthEnabled();
216217

217218
auto const serverAddresses = BindingInformation::Format(pConfiguration.QueryBindings(), pInProcessApplication->QueryApplicationVirtualPath());
218-
pIISCofigurationData->pwzBindings = SysAllocString(serverAddresses.c_str());
219+
pIISConfigurationData->pwzBindings = SysAllocString(serverAddresses.c_str());
220+
pIISConfigurationData->maxRequestBodySize = pInProcessApplication->QueryConfig().QueryMaxRequestBodySizeLimit();
219221
return S_OK;
220222
}
221223

src/Servers/IIS/IIS/benchmarks/IIS.Performance/PlaintextBenchmark.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class PlaintextBenchmark
2323
[GlobalSetup]
2424
public void Setup()
2525
{
26-
_server = TestServer.Create(builder => builder.UseMiddleware<PlaintextMiddleware>(), new LoggerFactory()).GetAwaiter().GetResult();
26+
_server = TestServer.Create(builder => builder.UseMiddleware<PlaintextMiddleware>(), new LoggerFactory(), new IISServerOptions()).GetAwaiter().GetResult();
2727
// Recreate client, TestServer.Client has additional logging that can hurt performance
2828
_client = new HttpClient()
2929
{

src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.netcoreapp3.0.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public IISServerOptions() { }
99
public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
1010
public string AuthenticationDisplayName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
1111
public bool AutomaticAuthentication { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
12+
public long? MaxRequestBodySize { get { throw null; } set { } }
1213
}
1314
}
1415
namespace Microsoft.AspNetCore.Hosting
@@ -27,6 +28,11 @@ public partial interface IServerVariablesFeature
2728
}
2829
namespace Microsoft.AspNetCore.Server.IIS
2930
{
31+
public sealed partial class BadHttpRequestException : System.IO.IOException
32+
{
33+
internal BadHttpRequestException() { }
34+
public int StatusCode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
35+
}
3036
public static partial class HttpContextExtensions
3137
{
3238
public static string GetIISServerVariable(this Microsoft.AspNetCore.Http.HttpContext context, string variableName) { throw null; }

src/Servers/IIS/IIS/src/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
using System.Runtime.CompilerServices;
55

66
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.IISIntegration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
7+
[assembly: InternalsVisibleTo("IIS.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
78

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.IO;
5+
using System.Runtime.CompilerServices;
6+
using Microsoft.AspNetCore.Http;
7+
8+
namespace Microsoft.AspNetCore.Server.IIS
9+
{
10+
public sealed class BadHttpRequestException : IOException
11+
{
12+
private BadHttpRequestException(string message, int statusCode, RequestRejectionReason reason)
13+
: base(message)
14+
{
15+
StatusCode = statusCode;
16+
Reason = reason;
17+
}
18+
19+
public int StatusCode { get; }
20+
21+
internal RequestRejectionReason Reason { get; }
22+
23+
internal static void Throw(RequestRejectionReason reason)
24+
{
25+
throw GetException(reason);
26+
}
27+
28+
[MethodImpl(MethodImplOptions.NoInlining)]
29+
internal static BadHttpRequestException GetException(RequestRejectionReason reason)
30+
{
31+
BadHttpRequestException ex;
32+
switch (reason)
33+
{
34+
case RequestRejectionReason.RequestBodyTooLarge:
35+
ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTooLarge, StatusCodes.Status413PayloadTooLarge, reason);
36+
break;
37+
default:
38+
ex = new BadHttpRequestException(CoreStrings.BadRequest, StatusCodes.Status400BadRequest, reason);
39+
break;
40+
}
41+
return ex;
42+
}
43+
}
44+
}

src/Servers/IIS/IIS/src/Core/IISConfigurationData.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ internal struct IISConfigurationData
1919
public bool fAnonymousAuthEnable;
2020
[MarshalAs(UnmanagedType.BStr)]
2121
public string pwzBindings;
22+
public int maxRequestBodySize;
2223
}
2324
}

src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Microsoft.AspNetCore.Http.Features.Authentication;
1717
using Microsoft.AspNetCore.Server.IIS.Core.IO;
1818
using Microsoft.AspNetCore.WebUtilities;
19+
using Microsoft.Extensions.Logging;
1920

2021
namespace Microsoft.AspNetCore.Server.IIS.Core
2122
{
@@ -28,7 +29,8 @@ internal partial class IISHttpContext : IFeatureCollection,
2829
IServerVariablesFeature,
2930
IHttpBufferingFeature,
3031
ITlsConnectionFeature,
31-
IHttpBodyControlFeature
32+
IHttpBodyControlFeature,
33+
IHttpMaxRequestBodySizeFeature
3234
{
3335
// NOTE: When feature interfaces are added to or removed from this HttpProtocol implementation,
3436
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
@@ -277,7 +279,7 @@ async Task<Stream> IHttpUpgradeFeature.UpgradeAsync()
277279
Debug.Assert(_readBodyTask == null || _readBodyTask.IsCompleted);
278280

279281
// Reset reading status to allow restarting with new IO
280-
_hasRequestReadingStarted = false;
282+
HasStartedConsumingRequestBody = false;
281283

282284
// Upgrade async will cause the stream processing to go into duplex mode
283285
AsyncIO = new WebSocketsAsyncIOEngine(_contextLock, _pInProcessHandler);
@@ -322,6 +324,35 @@ unsafe X509Certificate2 ITlsConnectionFeature.ClientCertificate
322324

323325
bool IHttpBodyControlFeature.AllowSynchronousIO { get; set; }
324326

327+
bool IHttpMaxRequestBodySizeFeature.IsReadOnly => HasStartedConsumingRequestBody || _wasUpgraded;
328+
329+
long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize
330+
{
331+
get => MaxRequestBodySize;
332+
set
333+
{
334+
if (HasStartedConsumingRequestBody)
335+
{
336+
throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedAfterRead);
337+
}
338+
if (_wasUpgraded)
339+
{
340+
throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedForUpgradedRequests);
341+
}
342+
if (value < 0)
343+
{
344+
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired);
345+
}
346+
347+
if (value > _options.IisMaxRequestSizeLimit)
348+
{
349+
_logger.LogWarning(CoreStrings.MaxRequestLimitWarning);
350+
}
351+
352+
MaxRequestBodySize = value;
353+
}
354+
}
355+
325356
void IHttpBufferingFeature.DisableRequestBuffering()
326357
{
327358
}

src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ internal partial class IISHttpContext
2828
private static readonly Type IISHttpContextType = typeof(IISHttpContext);
2929
private static readonly Type IServerVariablesFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IServerVariablesFeature);
3030
private static readonly Type IHttpBufferingFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature);
31+
private static readonly Type IHttpMaxRequestBodySizeFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
3132

3233
private object _currentIHttpRequestFeature;
3334
private object _currentIHttpResponseFeature;
@@ -48,6 +49,7 @@ internal partial class IISHttpContext
4849
private object _currentIHttpSendFileFeature;
4950
private object _currentIServerVariablesFeature;
5051
private object _currentIHttpBufferingFeature;
52+
private object _currentIHttpMaxRequestBodySizeFeature;
5153

5254
private void Initialize()
5355
{
@@ -61,6 +63,7 @@ private void Initialize()
6163
_currentIHttpAuthenticationFeature = this;
6264
_currentIServerVariablesFeature = this;
6365
_currentIHttpBufferingFeature = this;
66+
_currentIHttpMaxRequestBodySizeFeature = this;
6467
_currentITlsConnectionFeature = this;
6568
}
6669

@@ -146,6 +149,10 @@ internal object FastFeatureGet(Type key)
146149
{
147150
return _currentIHttpBufferingFeature;
148151
}
152+
if (key == IHttpMaxRequestBodySizeFeature)
153+
{
154+
return _currentIHttpMaxRequestBodySizeFeature;
155+
}
149156

150157
return ExtraFeatureGet(key);
151158
}
@@ -249,6 +256,10 @@ internal void FastFeatureSet(Type key, object feature)
249256
_currentIHttpBufferingFeature = feature;
250257
return;
251258
}
259+
if (key == IHttpMaxRequestBodySizeFeature)
260+
{
261+
_currentIHttpMaxRequestBodySizeFeature = feature;
262+
}
252263
if (key == IISHttpContextType)
253264
{
254265
throw new InvalidOperationException("Cannot set IISHttpContext in feature collection");
@@ -334,6 +345,10 @@ private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
334345
{
335346
yield return new KeyValuePair<Type, object>(IHttpBufferingFeature, _currentIHttpBufferingFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature);
336347
}
348+
if (_currentIHttpMaxRequestBodySizeFeature != null)
349+
{
350+
yield return new KeyValuePair<Type, object>(IHttpMaxRequestBodySizeFeature, _currentIHttpMaxRequestBodySizeFeature as global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
351+
}
337352

338353
if (MaybeExtra != null)
339354
{

src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
1414
{
1515
internal partial class IISHttpContext
1616
{
17+
private long _consumedBytes;
18+
1719
/// <summary>
1820
/// Reads data from the Input pipe to the user.
1921
/// </summary>
@@ -22,7 +24,7 @@ internal partial class IISHttpContext
2224
/// <returns></returns>
2325
internal async ValueTask<int> ReadAsync(Memory<byte> memory, CancellationToken cancellationToken)
2426
{
25-
if (!_hasRequestReadingStarted)
27+
if (!HasStartedConsumingRequestBody)
2628
{
2729
InitializeRequestIO();
2830
}
@@ -105,9 +107,15 @@ private async Task ReadBody()
105107
// Read was not canceled because of incoming write or IO stopping
106108
if (read != -1)
107109
{
110+
_consumedBytes += read;
108111
_bodyInputPipe.Writer.Advance(read);
109112
}
110113

114+
if (_consumedBytes > MaxRequestBodySize)
115+
{
116+
BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge);
117+
}
118+
111119
var result = await _bodyInputPipe.Writer.FlushAsync();
112120

113121
if (result.IsCompleted || result.IsCanceled)

src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ private static class Log
2020
private static readonly Action<ILogger, string, string, Exception> _unexpectedError =
2121
LoggerMessage.Define<string, string>(LogLevel.Error, new EventId(3, "UnexpectedError"), @"Unexpected exception in ""{ClassName}.{MethodName}"".");
2222

23+
private static readonly Action<ILogger, string, string, Exception> _connectionBadRequest =
24+
LoggerMessage.Define<string, string>(LogLevel.Information, new EventId(4, nameof(ConnectionBadRequest)), @"Connection id ""{ConnectionId}"" bad request data: ""{message}""");
25+
2326
public static void ConnectionDisconnect(ILogger logger, string connectionId)
2427
{
2528
_connectionDisconnect(logger, connectionId, null);
@@ -34,6 +37,11 @@ public static void UnexpectedError(ILogger logger, string className, Exception e
3437
{
3538
_unexpectedError(logger, className, methodName, ex);
3639
}
40+
41+
public static void ConnectionBadRequest(ILogger logger, string connectionId, BadHttpRequestException ex)
42+
{
43+
_connectionBadRequest(logger, connectionId, ex.Message, ex);
44+
}
3745
}
3846
}
3947
}

0 commit comments

Comments
 (0)