Skip to content

Commit

Permalink
feat(webapi): Limit Content-Length / request body size (#1416)
Browse files Browse the repository at this point in the history
<!--- Provide a general summary of your changes in the Title above -->

## Description

Large request bodies can be used as a form of DDOS, especially when it
comes to transmissions because they have a more complex hierarchy
validation
Limiting the body size on requests to 100 kB

## Related Issue(s)

- #1415 

## Verification

- [ ] **Your** code builds clean without any errors or warnings
- [ ] Manual testing done (required)
- [ ] Relevant automated test added (if you find this hard, leave it and
we'll help out)

## Documentation

- [ ] Documentation is updated (either in `docs`-directory, Altinnpedia
or a separate linked PR in
[altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs), if
applicable)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a maximum request body size limit to enhance server request
handling.
  
- **Bug Fixes**
- Improved error handling for invalid `endUserId` and unknown user
types, providing more informative responses.
- Standardized response generation for various error scenarios in the
`PatchDialogsController`.

- **Documentation**
- Enhanced clarity in error handling processes across multiple
middleware components.

- **Refactor**
- Streamlined error response construction methods for consistency and
efficiency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Magnus Sandgren <[email protected]>
  • Loading branch information
oskogstad and MagnusSandgren authored Nov 7, 2024
1 parent f1096a4 commit 44be20a
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public Task InvokeAsync(HttpContext context)
if (!NorwegianPersonIdentifier.TryParse(endUserIdQuery.First(), out var endUserId))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.WriteAsJsonAsync(context.ResponseBuilder(
context.Response.WriteAsJsonAsync(context.GetResponseOrDefault(
context.Response.StatusCode,
[
new("EndUserId",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task InvokeAsync(HttpContext context)
if (userType == UserIdType.Unknown)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsJsonAsync(context.ResponseBuilder(
await context.Response.WriteAsJsonAsync(context.GetResponseOrDefault(
context.Response.StatusCode,
[
new("Type",
Expand Down
1 change: 1 addition & 0 deletions src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ internal static class Constants
internal const string IfMatch = "If-Match";
internal const string Authorization = "Authorization";
internal const string CurrentTokenIssuer = "CurrentIssuer";
internal const int MaxRequestBodySize = 100_000;

internal static class SwaggerSummary
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,41 @@ namespace Digdir.Domain.Dialogporten.WebApi.Common.Extensions;

internal static class ErrorResponseBuilderExtensions
{
public static object ResponseBuilder(this HttpContext ctx, int statusCode, List<ValidationFailure>? failures = null) =>
ResponseBuilder(failures ?? [], ctx, statusCode);
public static ProblemDetails DefaultResponse(this HttpContext ctx, int? statusCode = null) => new()
{
Title = "An error occurred while processing the request.",
Detail = "Something went wrong during the request.",
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1",
Status = statusCode ?? ctx.Response.StatusCode,
Instance = ctx.Request.Path,
Extensions = { { "traceId", Activity.Current?.Id ?? ctx.TraceIdentifier } }
};

public static ProblemDetails GetResponseOrDefault(this HttpContext ctx, int statusCode,
List<ValidationFailure>? failures = null) =>
ctx.ResponseBuilder(failures, statusCode) ?? ctx.DefaultResponse(statusCode);

public static object ResponseBuilder(List<ValidationFailure> failures, HttpContext ctx, int statusCode)
=> ctx.ResponseBuilder(failures, statusCode) ?? ctx.DefaultResponse(statusCode);

public static ProblemDetails? ResponseBuilder(this HttpContext ctx, List<ValidationFailure>? failures = null, int? statusCode = null)
{
var errors = failures
var errors = failures?
.GroupBy(f => f.PropertyName)
.ToDictionary(x => x.Key, x => x.Select(m => m.ErrorMessage).ToArray());
.ToDictionary(x => x.Key, x => x.Select(m => m.ErrorMessage).ToArray())
?? [];

statusCode ??= ctx.Response.StatusCode;
return statusCode switch
{
StatusCodes.Status413PayloadTooLarge => new ProblemDetails
{
Title = $"Payload too large. The maximum allowed size is {Constants.MaxRequestBodySize} bytes.",
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.11",
Status = statusCode,
Instance = ctx.Request.Path,
Extensions = { { "traceId", Activity.Current?.Id ?? ctx.TraceIdentifier } }
},
StatusCodes.Status400BadRequest => new ValidationProblemDetails(errors)
{
Title = "One or more validation errors occurred.",
Expand Down Expand Up @@ -73,15 +98,7 @@ public static object ResponseBuilder(List<ValidationFailure> failures, HttpConte
Instance = ctx.Request.Path,
Extensions = { { "traceId", Activity.Current?.Id ?? ctx.TraceIdentifier } }
},
_ => new ProblemDetails
{
Title = "An error occurred while processing the request.",
Detail = "Something went wrong during the request.",
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1",
Status = ctx.Response.StatusCode,
Instance = ctx.Request.Path,
Extensions = { { "traceId", Activity.Current?.Id ?? ctx.TraceIdentifier } }
}
_ => null
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,26 @@ internal sealed class GlobalExceptionHandler : IExceptionHandler
public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception exception,
CancellationToken cancellationToken)
{
var http = $"{ctx.Request.Scheme}: {ctx.Request.Method} {ctx.Request.Path}";
var type = exception.GetType().Name;
var error = exception.Message;
var logger = ctx.Resolve<ILogger<GlobalExceptionHandler>>();
logger.LogError(exception, "{@Http}{@Type}{@Reason}", http, type, error);
ctx.Response.StatusCode = exception is IUpstreamServiceError
? StatusCodes.Status502BadGateway
: StatusCodes.Status500InternalServerError;
ctx.Response.StatusCode = exception switch
{
BadHttpRequestException badHttpRequestException => badHttpRequestException.StatusCode,
IUpstreamServiceError => StatusCodes.Status502BadGateway,
_ => StatusCodes.Status500InternalServerError
};

ctx.Response.ContentType = "application/problem+json";
await ctx.Response.WriteAsJsonAsync(ctx.ResponseBuilder(ctx.Response.StatusCode), cancellationToken);
var response = ctx.ResponseBuilder();

if (ctx.Response.StatusCode >= 500 || response is null)
{
var http = $"{ctx.Request.Scheme}: {ctx.Request.Method} {ctx.Request.Path}";
var type = exception.GetType().Name;
var error = exception.Message;
var logger = ctx.Resolve<ILogger<GlobalExceptionHandler>>();
logger.LogError(exception, "{@Http}{@Type}{@Reason}", http, type, error);
}

await ctx.Response.WriteAsJsonAsync(response ?? ctx.DefaultResponse(), cancellationToken);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ public async Task<IActionResult> Patch(
if (!dialogQueryResult.TryPickT0(out var dialog, out var errors))
{
return errors.Match<IActionResult>(
notFound => NotFound(HttpContext.ResponseBuilder(StatusCodes.Status404NotFound,
notFound => NotFound(HttpContext.GetResponseOrDefault(StatusCodes.Status404NotFound,
notFound.ToValidationResults())),
validationFailed =>
BadRequest(HttpContext.ResponseBuilder(StatusCodes.Status400BadRequest,
BadRequest(HttpContext.GetResponseOrDefault(StatusCodes.Status400BadRequest,
validationFailed.Errors.ToList())));
}

Expand All @@ -88,12 +88,12 @@ public async Task<IActionResult> Patch(
var result = await _sender.Send(command, ct);
return result.Match(
success => (IActionResult)NoContent(),
notFound => NotFound(HttpContext.ResponseBuilder(StatusCodes.Status404NotFound, notFound.ToValidationResults())),
badRequest => BadRequest(HttpContext.ResponseBuilder(StatusCodes.Status400BadRequest, badRequest.ToValidationResults())),
validationFailed => BadRequest(HttpContext.ResponseBuilder(StatusCodes.Status400BadRequest, validationFailed.Errors.ToList())),
forbidden => new ObjectResult(HttpContext.ResponseBuilder(StatusCodes.Status403Forbidden, forbidden.ToValidationResults())),
domainError => UnprocessableEntity(HttpContext.ResponseBuilder(StatusCodes.Status422UnprocessableEntity, domainError.ToValidationResults())),
concurrencyError => new ObjectResult(HttpContext.ResponseBuilder(StatusCodes.Status412PreconditionFailed)) { StatusCode = StatusCodes.Status412PreconditionFailed }
notFound => NotFound(HttpContext.GetResponseOrDefault(StatusCodes.Status404NotFound, notFound.ToValidationResults())),
badRequest => BadRequest(HttpContext.GetResponseOrDefault(StatusCodes.Status400BadRequest, badRequest.ToValidationResults())),
validationFailed => BadRequest(HttpContext.GetResponseOrDefault(StatusCodes.Status400BadRequest, validationFailed.Errors.ToList())),
forbidden => new ObjectResult(HttpContext.GetResponseOrDefault(StatusCodes.Status403Forbidden, forbidden.ToValidationResults())),
domainError => UnprocessableEntity(HttpContext.GetResponseOrDefault(StatusCodes.Status422UnprocessableEntity, domainError.ToValidationResults())),
concurrencyError => new ObjectResult(HttpContext.GetResponseOrDefault(StatusCodes.Status412PreconditionFailed)) { StatusCode = StatusCodes.Status412PreconditionFailed }
);
}
}
5 changes: 5 additions & 0 deletions src/Digdir.Domain.Dialogporten.WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura
{
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(kestrelOptions =>
{
kestrelOptions.Limits.MaxRequestBodySize = Constants.MaxRequestBodySize;
});

builder.Host.UseSerilog((context, services, configuration) => configuration
.MinimumLevel.Warning()
.ReadFrom.Configuration(context.Configuration)
Expand Down

0 comments on commit 44be20a

Please sign in to comment.