Skip to content

Commit bee3290

Browse files
Properly reject non-json FromBody parameter binding (#35976)
1 parent cc5f896 commit bee3290

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

+21-1
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,12 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
558558
var feature = httpContext.Features.Get<IHttpRequestBodyDetectionFeature>();
559559
if (feature?.CanHaveBody == true)
560560
{
561+
if (!httpContext.Request.HasJsonContentType())
562+
{
563+
Log.UnexpectedContentType(httpContext, httpContext.Request.ContentType);
564+
httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
565+
return;
566+
}
561567
try
562568
{
563569
bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType);
@@ -590,6 +596,12 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
590596
var feature = httpContext.Features.Get<IHttpRequestBodyDetectionFeature>();
591597
if (feature?.CanHaveBody == true)
592598
{
599+
if (!httpContext.Request.HasJsonContentType())
600+
{
601+
Log.UnexpectedContentType(httpContext, httpContext.Request.ContentType);
602+
httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
603+
return;
604+
}
593605
try
594606
{
595607
bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType);
@@ -603,7 +615,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
603615
{
604616

605617
Log.RequestBodyInvalidDataException(httpContext, ex, factoryContext.ThrowOnBadRequest);
606-
httpContext.Response.StatusCode = 400;
618+
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
607619
return;
608620
}
609621
}
@@ -1204,6 +1216,14 @@ public static void RequiredParameterNotProvided(HttpContext httpContext, string
12041216
[LoggerMessage(4, LogLevel.Debug, RequiredParameterNotProvidedLogMessage, EventName = "RequiredParameterNotProvided")]
12051217
private static partial void RequiredParameterNotProvided(ILogger logger, string parameterType, string parameterName, string source);
12061218

1219+
public static void UnexpectedContentType(HttpContext httpContext, string? contentType)
1220+
=> UnexpectedContentType(GetLogger(httpContext), contentType ?? "(none)");
1221+
1222+
[LoggerMessage(6, LogLevel.Debug,
1223+
"Expected a supported JSON media type but got \"{ContentType}\".",
1224+
EventName = "UnexpectedContentType")]
1225+
private static partial void UnexpectedContentType(ILogger logger, string contentType);
1226+
12071227
private static ILogger GetLogger(HttpContext httpContext)
12081228
{
12091229
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

+69
Original file line numberDiff line numberDiff line change
@@ -2360,7 +2360,65 @@ public async Task CanExecuteRequestDelegateWithResultsExtension()
23602360
Assert.False(httpContext.RequestAborted.IsCancellationRequested);
23612361
var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray());
23622362
Assert.Equal(@"""Hello Tester. This is from an extension method.""", decodedResponseBody);
2363+
}
2364+
2365+
[Fact]
2366+
public async Task RequestDelegateRejectsNonJsonContent()
2367+
{
2368+
var httpContext = new DefaultHttpContext();
2369+
httpContext.Request.Headers["Content-Type"] = "application/xml";
2370+
httpContext.Request.Headers["Content-Length"] = "1";
2371+
httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
2372+
2373+
var serviceCollection = new ServiceCollection();
2374+
serviceCollection.AddSingleton(LoggerFactory);
2375+
httpContext.RequestServices = serviceCollection.BuildServiceProvider();
2376+
2377+
var factoryResult = RequestDelegateFactory.Create((HttpContext context, Todo todo) =>
2378+
{
2379+
});
2380+
var requestDelegate = factoryResult.RequestDelegate;
2381+
2382+
await requestDelegate(httpContext);
2383+
2384+
Assert.Equal(415, httpContext.Response.StatusCode);
2385+
var logMessage = Assert.Single(TestSink.Writes);
2386+
Assert.Equal(new EventId(6, "UnexpectedContentType"), logMessage.EventId);
2387+
Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
2388+
}
2389+
2390+
[Fact]
2391+
public async Task RequestDelegateWithBindAndImplicitBodyRejectsNonJsonContent()
2392+
{
2393+
Todo originalTodo = new()
2394+
{
2395+
Name = "Write more tests!"
2396+
};
2397+
2398+
var httpContext = new DefaultHttpContext();
2399+
2400+
var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo);
2401+
var stream = new MemoryStream(requestBodyBytes);
2402+
httpContext.Request.Body = stream;
2403+
httpContext.Request.Headers["Content-Type"] = "application/xml";
2404+
httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture);
2405+
httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
2406+
2407+
var serviceCollection = new ServiceCollection();
2408+
serviceCollection.AddSingleton(LoggerFactory);
2409+
httpContext.RequestServices = serviceCollection.BuildServiceProvider();
2410+
2411+
var factoryResult = RequestDelegateFactory.Create((HttpContext context, JsonTodo customTodo, Todo todo) =>
2412+
{
2413+
});
2414+
var requestDelegate = factoryResult.RequestDelegate;
2415+
2416+
await requestDelegate(httpContext);
23632417

2418+
Assert.Equal(415, httpContext.Response.StatusCode);
2419+
var logMessage = Assert.Single(TestSink.Writes);
2420+
Assert.Equal(new EventId(6, "UnexpectedContentType"), logMessage.EventId);
2421+
Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
23642422
}
23652423

23662424
private DefaultHttpContext CreateHttpContext()
@@ -2399,6 +2457,17 @@ private class CustomTodo : Todo
23992457
}
24002458
}
24012459

2460+
private class JsonTodo : Todo
2461+
{
2462+
public static async ValueTask<JsonTodo?> BindAsync(HttpContext context, ParameterInfo parameter)
2463+
{
2464+
// manually call deserialize so we don't check content type
2465+
var body = await JsonSerializer.DeserializeAsync<JsonTodo>(context.Request.Body);
2466+
context.Request.Body.Position = 0;
2467+
return body;
2468+
}
2469+
}
2470+
24022471
private record struct TodoStruct(int Id, string? Name, bool IsComplete) : ITodo;
24032472

24042473
private interface ITodo

0 commit comments

Comments
 (0)