发布于 2026-01-06 11 阅读
0

纵深防御:设计 HTTP 内容长度限制中间件 - ASP.NET 5(或 .NET Core)

纵深防御:设计 HTTP 内容长度限制中间件 - ASP.NET 5(或 .NET Core)

什么?

我们希望设计一个中间件,当它插入到 ASP.NET 5(或 .NET Core)应用程序管道中时,可以限制输入有效负载的大小,这样,依赖于发送更大(或大于我们指定大小)的有效负载的攻击就可以被应用程序本身拒绝。

为什么?

你可能会说,我已经有了防火墙和其他安全和网络设备,为什么还需要在应用层实现这个功能呢?

如果你了解纵深防御,那么美国计算机应急响应小组(US-CERT)对冗余安全机制有如下论述:

在应用程序中采用多层安全防御可以降低攻击成功的概率。引入冗余的安全机制,攻击者必须绕过每层机制才能访问数字资产。例如,带有身份验证检查的软件系统可以阻止绕过防火墙的攻击者。使用多层防御来保护应用程序可以防止出现单点故障,从而避免应用程序安全受到威胁。

如何实现?——让我们开始编程吧!

所以我把这个设计分解成了3个部分:

  1. 创建基本逻辑
  2. 使其成为可重用的中间件
  3. 使其看起来像一个原生中间件

那么,让我们开始吧:

1. 创建基本逻辑

我们想检查ContentLength有效Request载荷是否超过我们的限制,如果超过,则应HTTP 413 Entity Too Large按照IETF RFC 7231 HTTP 规范发送。

if (httpContext.Request.ContentLength > SOME_LIMIT)
{    
    httpContext.Response.StatusCode = StatusCodes.Status413RequestEntityTooLarge;
    await httpContext.Response.WriteAsJsonAsync(new
    {
        Title = "Request too large",
        Status = StatusCodes.Status413RequestEntityTooLarge,
        Type = "https://tools.ietf.org/html/rfc7231#section-6.5.11",
    });
    await httpContext.Response.CompleteAsync();
}
else
{
    await _requestDelegate.Invoke(httpContext);
}
```
{% endraw %}

Let's break it down:

* First we're checking the content length.
* If it's greater than our limit, we're writing a {% raw %}`JSON`{% endraw %} response with {% raw %}`HTTP 413`{% endraw %}. And completing the response as we don't need to execute any further Middleware.
* If not, we can continue the Middleware pipe and execute next Middleware.

### 2. Create a re-usable middleware
Now we have our basic logic ready, let's create a re-usable middleware out of it.
We need to do following:
1. Create a Middleware class and run our logic in {% raw %}`Invoke`{% endraw %} or {% raw %}`InvokeAsync`{% endraw %} method.
2. Take input at the runtime instead of hard-coding it.
3. Add logging

Let's complete this one by one:

#### 2.1.  Create a Middleware class and run our logic in {% raw %}`Invoke`{% endraw %} or {% raw %}`InvokeAsync`{% endraw %} method.
Here's a basic structure of a Middleware class looks like. It should have an {% raw %}`Invoke`{% endraw %} or {% raw %}`InvokeAsync`{% endraw %} method which would be called by runtime based on our configuration.
{% raw %}


````csharp
public class ContentLengthRestrictionMiddleware
{
    private readonly RequestDelegate _requestDelegate;

    public ContentLengthRestrictionMiddleware(RequestDelegate nextRequestDelegate)
    {
        _requestDelegate = nextRequestDelegate;        
    }
    public async Task InvokeAsync(HttpContext httpContext)
    {
        if (httpContext.Request.ContentLength > SOME_LIMIT)
        {
            httpContext.Response.StatusCode = StatusCodes.Status413RequestEntityTooLarge;
            await httpContext.Response.WriteAsJsonAsync(new
            {
                Title = "Request too large",
                Status = StatusCodes.Status413RequestEntityTooLarge,
                Type = "https://tools.ietf.org/html/rfc7231#section-6.5.11",
            });
            await httpContext.Response.CompleteAsync();
        }
        else
        {
            await _requestDelegate.Invoke(httpContext);
        }
    }
}                  
```
{% endraw %}

#### 2.2. Take input at the runtime instead of hard-coding it.
Currently we're hard-coding the limit ({% raw %}`SOME_LIMIT`{% endraw %}), we can create a class and take input at runtime.
{% raw %}

````csharp
public class ContentLengthRestrictionOptions
{
    public long ContentLengthLimit { get; set; }
}
```
{% endraw %}

And modify our Middleware to use this:
{% raw %}


````csharp
public class ContentLengthRestrictionMiddleware
{
    private readonly ContentLengthRestrictionOptions _contentLengthRestrictionOptions;
    private readonly RequestDelegate _requestDelegate;

    public ContentLengthRestrictionMiddleware(RequestDelegate nextRequestDelegate, ContentLengthRestrictionOptions contentLengthRestrictionOptions)
    {
        _requestDelegate = nextRequestDelegate;
        _contentLengthRestrictionOptions = contentLengthRestrictionOptions;        
    }
    public async Task InvokeAsync(HttpContext httpContext)
    {
        if (_contentLengthRestrictionOptions != null && _contentLengthRestrictionOptions.ContentLengthLimit > 0 && httpContext.Request.ContentLength > _contentLengthRestrictionOptions.ContentLengthLimit)
        {
       httpContext.Response.StatusCode = StatusCodes.Status413RequestEntityTooLarge;
            await httpContext.Response.WriteAsJsonAsync(new
            {
                Title = "Request too large",
                Status = StatusCodes.Status413RequestEntityTooLarge,
                Type = "https://tools.ietf.org/html/rfc7231#section-6.5.11",
            });
            await httpContext.Response.CompleteAsync();
        }
        else
        {
            await _requestDelegate.Invoke(httpContext);
        }
    }
}
```
{% endraw %}


#### 2.3. Add logging

Here's little tricky part, you can't just inject an {% raw %}`ILogger<T>`{% endraw %} and expect runtime to give you that dependency, you can get an {% raw %}`ILoggerFactory`{% endraw %} and then you can create your own {% raw %}`ILogger<T>`{% endraw %}.
{% raw %}


````csharp
public class ContentLengthRestrictionMiddleware
{
    private readonly ContentLengthRestrictionOptions _contentLengthRestrictionOptions;
    private readonly ILogger<ContentLengthRestrictionMiddleware> _logger;
    private readonly RequestDelegate _requestDelegate;

    public ContentLengthRestrictionMiddleware(RequestDelegate nextRequestDelegate, ContentLengthRestrictionOptions contentLengthRestrictionOptions, ILoggerFactory loggerFactory)
    {
        _requestDelegate = nextRequestDelegate;
        _contentLengthRestrictionOptions = contentLengthRestrictionOptions;
        _logger = loggerFactory.CreateLogger<ContentLengthRestrictionMiddleware>();
    }
    public async Task InvokeAsync(HttpContext httpContext)
    {
        if (_contentLengthRestrictionOptions != null && _contentLengthRestrictionOptions.ContentLengthLimit > 0 && httpContext.Request.ContentLength > _contentLengthRestrictionOptions.ContentLengthLimit)
        {
            _logger.LogWarning("Rejecting request with Content-Length {0} more than allowed {1}.", httpContext.Request.ContentLength, _contentLengthRestrictionOptions.ContentLengthLimit);
            httpContext.Response.StatusCode = StatusCodes.Status413RequestEntityTooLarge;
            await httpContext.Response.WriteAsJsonAsync(new
            {
                Title = "Request too large",
                Status = StatusCodes.Status413RequestEntityTooLarge,
                Type = "https://tools.ietf.org/html/rfc7231#section-6.5.11",
            });
            await httpContext.Response.CompleteAsync();
        }
        else
        {
            await _requestDelegate.Invoke(httpContext);
        }
    }
}
```
{% endraw %}

I've added a log only when the Middleware rejects the requests.

### 3. Make it look like a native Middleware.
We can create an Extension method on {% raw %}`IApplicationBuilder`{% endraw %}, something to call like {% raw %}`UseXXX`{% endraw %}, so it feels like a Native Middleware.
{% raw %}


````csharp
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseContentLengthRestriction(this IApplicationBuilder builder, ContentLengthRestrictionOptions contentLengthRestrictionOptions)
        => builder.UseMiddleware<ContentLengthRestrictionMiddleware>(contentLengthRestrictionOptions);
}
```
{% endraw %}

And we can use it like:
{% raw %}


````csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //This is our Middleware 😍
    app.UseContentLengthRestriction(new ContentLengthRestrictionOptions
    {
        ContentLengthLimit = 10
    });

    /// Other configuration
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```
{% endraw %}


## 4. The Execution
Sending a {% raw %}`POST`{% endraw %} request to our endpoint:
{% raw %}

````curl
curl -X POST "https://localhost:5001/WeatherForecast" -H  "accept: */*" -H  "Content-Type: application/json" -d "{\"date\":\"2021-08-22T14:17:58.115Z\",\"temperatureC\":0,\"summary\":\"string\"}"
```
{% endraw %}


Results in following:
{% raw %}

````json
{
  "title": "Request too large",
  "status": 413,
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.11"
}
```
{% endraw %}


And logs following in to stdout:
{% raw %}

Enter fullscreen mode Exit fullscreen mode

警告:AwesomeApi.ContentLengthRestrictionMiddleware[0]
拒绝 Content-Length 为 71 的请求,超过允许的 10。




## Conclusion
That's it! Congratulations! You just designed and created a custom Middleware! Give yourself a pat in the back. Don't forget to stretch your shoulders and neck once in a while.

Happy Coding!

## Code
Source Code is available at:
[https://github.com/iSatishYadav/ContentLengthRestrictionMiddleware](https://github.com/iSatishYadav/ContentLengthRestrictionMiddleware)

If you liked it, go show some love to the repo and star it. 

Originally Posted at my blog:
[https://blog.satishyadav.com/defense-in-depth-designing-an-http-content-length-restriction-middleware-asp-net-5-or-net-core](https://blog.satishyadav.com/defense-in-depth-designing-an-http-content-length-restriction-middleware-asp-net-5-or-net-core?utm_source=dt)
Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/satish/defense-in-depth-designing-an-http-content-length-restriction-middleware-asp-net-5-or-net-core-1cpp