Skip to content

Commit

Permalink
NLogRequestPostedBodyMiddleware - Only accept AllowContentTypes
Browse files Browse the repository at this point in the history
  • Loading branch information
snakefoot committed Jun 4, 2022
1 parent a234cfc commit 0d210c2
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 35 deletions.
32 changes: 19 additions & 13 deletions src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,30 @@ namespace NLog.Web
/// POST request body
///
/// Usage: app.UseMiddleware<NLogRequestPostBodyMiddleware>(); where app is an IApplicationBuilder
/// Register the NLogRequestPostBodyMiddlewareOption in the IoC so that the config gets passed to the constructor
///
/// Inject the NLogRequestPostBodyMiddlewareOption in the IoC if wanting to override default values for constructor
/// </summary>
public class NLogRequestPostedBodyMiddleware
{
private readonly RequestDelegate _next;

private readonly NLogRequestPostedBodyMiddlewareOptions _options;

/// <summary>
/// Constructor that takes a configuration
/// Initializes new instance of the <see cref="NLogRequestPostedBodyMiddleware"/> class
/// </summary>
/// <param name="next"></param>
/// <param name="options"></param>
public NLogRequestPostedBodyMiddleware(RequestDelegate next, NLogRequestPostedBodyMiddlewareOptions options)
/// <remarks>
/// Use the following in Startup.cs:
/// <code>
/// public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
/// {
/// app.UseMiddleware&lt;NLog.Web.NLogRequestPostedBodyMiddleware&gt;();
/// }
/// </code>
/// </remarks>
public NLogRequestPostedBodyMiddleware(RequestDelegate next, NLogRequestPostedBodyMiddlewareOptions options = default)
{
_next = next;
_options = options;
_options = options ?? NLogRequestPostedBodyMiddlewareOptions.Default;
}

/// <summary>
Expand Down Expand Up @@ -62,30 +69,26 @@ private bool ShouldCaptureRequestBody(HttpContext context)
if (context == null)
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext is null");
// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
return false;
}

if (context.Request == null)
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext.Request is null");
// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
return false;
}

// Perform null checking
if (context.Request.Body == null)
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext.Request.Body stream is null");
// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
return false;
}

// If we cannot read the stream we cannot capture the body
if (!context.Request.Body.CanRead)
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext.Request.Body stream is non-readable");
// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
return false;
}

Expand All @@ -101,11 +104,14 @@ private bool ShouldCaptureRequestBody(HttpContext context)
/// <returns>The contents of the Stream read fully from start to end as a String</returns>
private async Task<string> GetString(Stream stream)
{
string responseText = null;

if (!stream.CanSeek)
return responseText;

// Save away the original stream position
var originalPosition = stream.Position;

string responseText = null;

try
{
// This is required to reset the stream position to the beginning in order to properly read all of the stream.
Expand Down
44 changes: 36 additions & 8 deletions src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddlewareOptions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using NLog.Common;
using NLog.Web.Internal;

namespace NLog.Web
{
Expand All @@ -19,16 +22,29 @@ public class NLogRequestPostedBodyMiddlewareOptions
public NLogRequestPostedBodyMiddlewareOptions()
{
ShouldCapture = DefaultCapture;
AllowContentTypes = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("application/", "json"),
new KeyValuePair<string, string>("text/", ""),
new KeyValuePair<string, string>("", "charset"),
new KeyValuePair<string, string>("application/", "xml"),
new KeyValuePair<string, string>("application/", "html")
};
}

/// <summary>
/// The maximum request size that will be captured
/// Defaults to 30KB. This checks against the ContentLength.
/// The maximum request posted body size that will be captured. Defaults to 30KB.
/// </summary>
/// <remarks>
/// HttpRequest.EnableBuffer() writes the request to TEMP files on disk if the request ContentLength is > 30KB
/// but uses memory otherwise if &lt;= 30KB, so we should protect against "very large"
/// request post body payloads.
/// but uses memory otherwise if &lt;= 30KB, so we should protect against "very large" request post body payloads.
/// </remarks>
public int MaxContentLength { get; set; } = 30 * 1024;

/// <summary>
/// Prefix and suffix values to be accepted as ContentTypes. Ex. key-prefix = "application/" and value-suffix = "json"
/// </summary>
public int MaximumRequestSize { get; set; } = 30 * 1024;
public IList<KeyValuePair<string,string>> AllowContentTypes { get; set; }

/// <summary>
/// If this returns true, the post request body will be captured
Expand All @@ -40,12 +56,24 @@ public NLogRequestPostedBodyMiddlewareOptions()
public Predicate<HttpContext> ShouldCapture { get; set; }

/// <summary>
/// The default predicate for ShouldCapture
/// Returns true if content length &lt;= 30KB
/// The default predicate for ShouldCapture. Returns true if content length &lt;= 30KB
/// </summary>
private bool DefaultCapture(HttpContext context)
{
return context?.Request?.ContentLength != null && context?.Request?.ContentLength <= MaximumRequestSize;
var contentLength = context?.Request?.ContentLength ?? 0;
if (contentLength <= 0 || contentLength > MaxContentLength)
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext.Request.ContentLength={0}", contentLength);
return false;
}

if (!context.HasAllowedContentType(AllowContentTypes))
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext.Request.ContentType={0}", context?.Request?.ContentType);
return false;
}

return true;
}
}
}
50 changes: 41 additions & 9 deletions src/NLog.Web/NLogRequestPostedBodyModule.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web;
using NLog.Common;
using NLog.Web.Internal;
using NLog.Web.LayoutRenderers;

namespace NLog.Web
Expand All @@ -12,6 +14,31 @@ namespace NLog.Web
/// </summary>
public class NLogRequestPostedBodyModule : IHttpModule
{
/// <summary>
/// The maximum request posted body size that will be captured. Defaults to 30KB.
/// </summary>
public int MaxContentLength { get; set; } = 30 * 1024;

/// <summary>
/// Prefix and suffix values to be accepted as ContentTypes. Ex. key-prefix = "application/" and value-suffix = "json"
/// </summary>
public IList<KeyValuePair<string, string>> AllowContentTypes { get; set; }

/// <summary>
/// Initializes new instance of the <see cref="NLogRequestPostedBodyModule"/> class
/// </summary>
public NLogRequestPostedBodyModule()
{
AllowContentTypes = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("application/", "json"),
new KeyValuePair<string, string>("text/", ""),
new KeyValuePair<string, string>("", "charset"),
new KeyValuePair<string, string>("application/", "xml"),
new KeyValuePair<string, string>("application/", "html")
};
}

void IHttpModule.Init(HttpApplication context)
{
context.BeginRequest += (sender, args) => OnBeginRequest((sender as HttpApplication)?.Context);
Expand All @@ -36,39 +63,47 @@ private bool ShouldCaptureRequestBody(HttpContext context)
if (context == null)
{
InternalLogger.Debug("NLogRequestPostedBodyModule: HttpContext is null");
// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
return false;
}

// Perform null checking
if (context.Request == null)
{
InternalLogger.Debug("NLogRequestPostedBodyModule: HttpContext.Request stream is null");
// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
return false;
}

var stream = context.Request.InputStream;
if (stream == null)
{
InternalLogger.Debug("NLogRequestPostedBodyModule: HttpContext.Request.Body stream is null");
// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
return false;
}

// If we cannot read the stream we cannot capture the body
if (!stream.CanRead)
{
InternalLogger.Debug("NLogRequestPostedBodyModule: HttpContext.Request.Body stream is non-readable");
// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
return false;
}

// If we cannot seek the stream we cannot capture the body
if (!stream.CanSeek)
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpApplication.HttpContext.Request.Body stream is non-seekable");
// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
InternalLogger.Debug("NLogRequestPostedBodyModule: HttpApplication.HttpContext.Request.Body stream is non-seekable");
return false;
}

var contentLength = context.Request.ContentLength;
if (contentLength <= 0 || contentLength > MaxContentLength)
{
InternalLogger.Debug("NLogRequestPostedBodyModule: HttpContext.Request.ContentLength={0}", contentLength);
return false;
}

if (!context.HasAllowedContentType(AllowContentTypes))
{
InternalLogger.Debug("NLogRequestPostedBodyModule: HttpContext.Request.ContentType={0}", context?.Request?.ContentType);
return false;
}

Expand All @@ -84,9 +119,6 @@ private string GetString(Stream stream)
{
string responseText = null;

if (stream.Length == 0)
return responseText;

// Save away the original stream position
var originalPosition = stream.Position;

Expand Down
27 changes: 27 additions & 0 deletions src/Shared/Internal/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
#if !ASP_NET_CORE
using System.Web;
#else
Expand Down Expand Up @@ -136,5 +137,31 @@ internal static ISession TryGetSession(this HttpContext context)
}
}
#endif

internal static bool HasAllowedContentType(this HttpContext context, IList<KeyValuePair<string, string>> allowContentTypes)
{
if (allowContentTypes?.Count > 0)
{
var contentType = context?.Request?.ContentType;
if (!string.IsNullOrEmpty(contentType))
{
for (int i = 0; i < allowContentTypes.Count; ++i)
{
var allowed = allowContentTypes[i];
if (contentType.StartsWith(allowed.Key, StringComparison.OrdinalIgnoreCase))
{
if (contentType.IndexOf(allowed.Value, StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
}
}

return false;
}

return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public void SetMaximumRequestSizeTest()
{
var config = new NLogRequestPostedBodyMiddlewareOptions();
var size = new Random().Next();
config.MaximumRequestSize = size;
config.MaxContentLength = size;

Assert.Equal(size, config.MaximumRequestSize);
Assert.Equal(size, config.MaxContentLength);
}

[Fact]
Expand All @@ -34,7 +34,8 @@ public void DefaultCaptureTrue()

HttpRequest request = Substitute.For<HttpRequest>();

request.ContentLength.Returns(NLogRequestPostedBodyMiddlewareOptions.Default.MaximumRequestSize - 1);
request.ContentLength.Returns(NLogRequestPostedBodyMiddlewareOptions.Default.MaxContentLength - 1);
request.ContentType.Returns(";charset=utf8");

httpContext.Request.Returns(request);

Expand Down Expand Up @@ -66,7 +67,7 @@ public void DefaultCaptureExcessiveContentLength()

HttpRequest request = Substitute.For<HttpRequest>();

request.ContentLength.Returns(NLogRequestPostedBodyMiddlewareOptions.Default.MaximumRequestSize + 1);
request.ContentLength.Returns(NLogRequestPostedBodyMiddlewareOptions.Default.MaxContentLength + 1);

httpContext.Request.Returns(request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ public void SuccessTest()
byte[] bodyBytes = Encoding.UTF8.GetBytes("This is a test request body");
defaultContext.Request.Body.Write(bodyBytes,0,bodyBytes.Length);
defaultContext.Request.ContentLength = bodyBytes.Length;
defaultContext.Request.ContentType = "text/plain";

// Act

long streamBeforePosition = defaultContext.Request.Body.Position;

var middlewareInstance = new NLogRequestPostedBodyMiddleware(Next, NLogRequestPostedBodyMiddlewareOptions.Default);
Expand Down
1 change: 1 addition & 0 deletions tests/NLog.Web.Tests/NLogRequestPostedBodyModuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public void HttpRequestBodyTest()
var expectedMessage = "Expected message";
MyWorkerRequest myRequest = new MyWorkerRequest(expectedMessage);
HttpContext httpContext = new HttpContext(myRequest);
httpContext.Request.ContentType = ";charset=utf8";

// Act
var httpModule = new NLogRequestPostedBodyModule();
Expand Down

0 comments on commit 0d210c2

Please sign in to comment.