Skip to content

Commit 4fbc70e

Browse files
authored
Merge pull request #35571 from dotnet/wadepickett/35237JsonPatchv10prev4
JsonPatch System.Text.Json - based .NET 10 Preview 4 update - Rewrite plus recode.
2 parents 2f07db0 + 511a0b6 commit 4fbc70e

File tree

21 files changed

+636
-381
lines changed

21 files changed

+636
-381
lines changed

aspnetcore/web-api/jsonpatch.md

Lines changed: 225 additions & 153 deletions
Large diffs are not rendered by default.

aspnetcore/web-api/jsonpatch/includes/jsonpatch9.md

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

33
This article explains how to handle JSON Patch requests in an ASP.NET Core web API.
44

5+
> [!IMPORTANT]
6+
> The JSON Patch standard has ***inherent security risks***. This implementation ***doesn't attempt to mitigate these inherent security risks***. It's the responsibility of the developer to ensure that the JSON Patch document is safe to apply to the target object. For more information, see the [Mitigating Security Risks](#mitigating-security-risks) section.
7+
58
## Package installation
69

7-
JSON Patch support in ASP.NET Core web API is based on `Newtonsoft.Json` and requires the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package. To enable JSON Patch support:
10+
JSON Patch support in ASP.NET Core web API is based on `Newtonsoft.Json` and requires the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package.
11+
12+
To enable JSON Patch support:
813

914
* Install the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package.
1015
* Call <xref:Microsoft.Extensions.DependencyInjection.NewtonsoftJsonMvcBuilderExtensions.AddNewtonsoftJson%2A>. For example:
@@ -234,6 +239,53 @@ To test the sample, run the app and send HTTP requests with the following settin
234239
* Header: `Content-Type: application/json-patch+json`
235240
* Body: Copy and paste one of the JSON patch document samples from the *JSON* project folder.
236241

242+
## Mitigating security risks
243+
244+
When using the `Microsoft.AspNetCore.JsonPatch` package with the `Newtonsoft.Json`-based implementation, it's critical to understand and mitigate potential security risks. The following sections outline the identified security risks associated with JSON Patch and provide recommended mitigations to ensure secure usage of the package.
245+
246+
> [!IMPORTANT]
247+
> ***This is not an exhaustive list of threats.*** App developers must conduct their own threat model reviews to determine an app-specific comprehensive list and come up with appropriate mitigations as needed. For example, apps which expose collections to patch operations should consider the potential for algorithmic complexity attacks if those operations insert or remove elements at the beginning of the collection.
248+
249+
By running comprehensive threat models for their own apps and addressing identified threats while following the recommended mitigations below, consumers of these packages can integrate JSON Patch functionality into their apps while minimizing security risks.
250+
251+
### Denial of Service (DoS) via memory amplification
252+
253+
* **Scenario**: A malicious client submits a `copy` operation that duplicates large object graphs multiple times, leading to excessive memory consumption.
254+
* **Impact**: Potential Out-Of-Memory (OOM) conditions, causing service disruptions.
255+
* **Mitigation**:
256+
* Validate incoming JSON Patch documents for size and structure before calling `ApplyTo`.
257+
* The validation needs to be app specific, but an example validation can look similar to the following:
258+
259+
```csharp
260+
public void Validate(JsonPatchDocument patch)
261+
{
262+
// This is just an example. It's up to the developer to make sure that
263+
// this case is handled properly, based on the app needs.
264+
if (patch.Operations.Where(op => op.OperationType == OperationType.Copy).Count()
265+
> MaxCopyOperationsCount)
266+
{
267+
throw new InvalidOperationException();
268+
}
269+
}
270+
```
271+
272+
### Business Logic Subversion
273+
274+
* **Scenario**: Patch operations can manipulate fields with implicit invariants (for example, internal flags, IDs, or computed fields), violating business constraints.
275+
* **Impact**: Data integrity issues and unintended app behavior.
276+
* **Mitigation**:
277+
* Use POCO objects with explicitly defined properties that are safe to modify.
278+
* Avoid exposing sensitive or security-critical properties in the target object.
279+
* If no POCO object is used, validate the patched object after applying operations to ensure business rules and invariants aren't violated.
280+
281+
### Authentication and authorization
282+
283+
* **Scenario**: Unauthenticated or unauthorized clients send malicious JSON Patch requests.
284+
* **Impact**: Unauthorized access to modify sensitive data or disrupt app behavior.
285+
* **Mitigation**:
286+
* Protect endpoints accepting JSON Patch requests with proper authentication and authorization mechanisms.
287+
* Restrict access to trusted clients or users with appropriate permissions.
288+
237289
## Additional resources
238290

239291
* [IETF RFC 5789 PATCH method specification](https://tools.ietf.org/html/rfc5789)
@@ -247,6 +299,9 @@ To test the sample, run the app and send HTTP requests with the following settin
247299

248300
This article explains how to handle JSON Patch requests in an ASP.NET Core web API.
249301

302+
> [!IMPORTANT]
303+
> The JSON Patch standard has ***inherent security risks***. Since these risks are inherent to the JSON Patch standard, this implementation ***doesn't attempt to mitigate inherent security risks***. It's the responsibility of the developer to ensure that the JSON Patch document is safe to apply to the target object. For more information, see the [Mitigating Security Risks](#mitigating-security-risks) section.
304+
250305
## Package installation
251306

252307
To enable JSON Patch support in your app, complete the following steps:
@@ -476,11 +531,5 @@ To test the sample, run the app and send HTTP requests with the following settin
476531
* Header: `Content-Type: application/json-patch+json`
477532
* Body: Copy and paste one of the JSON patch document samples from the *JSON* project folder.
478533

479-
## Additional resources
480-
481-
* [IETF RFC 5789 PATCH method specification](https://tools.ietf.org/html/rfc5789)
482-
* [IETF RFC 6902 JSON Patch specification](https://tools.ietf.org/html/rfc6902)
483-
* [IETF RFC 6901 JSON Patch path format spec](https://tools.ietf.org/html/rfc6901)
484-
* [ASP.NET Core JSON Patch source code](https://github.com/dotnet/AspNetCore/tree/main/src/Features/JsonPatch/src)
485-
486534
:::moniker-end
535+
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
3+
4+
using App.Data;
5+
using App.Models;
6+
7+
namespace App.Controllers;
8+
9+
[ApiController]
10+
[Route("/api/customers")]
11+
public class CustomerController : ControllerBase
12+
{
13+
[HttpGet("{id}", Name = "GetCustomer")]
14+
public Customer Get(AppDb db, string id)
15+
{
16+
// Retrieve the customer by ID
17+
var customer = db.Customers.FirstOrDefault(c => c.Id == id);
18+
19+
// Return 404 Not Found if customer doesn't exist
20+
if (customer == null)
21+
{
22+
Response.StatusCode = 404;
23+
return null;
24+
}
25+
26+
return customer;
27+
}
28+
29+
// <snippet_PatchAction>
30+
[HttpPatch("{id}", Name = "UpdateCustomer")]
31+
public IActionResult Update(AppDb db, string id, [FromBody] JsonPatchDocument<Customer> patchDoc)
32+
{
33+
// Retrieve the customer by ID
34+
var customer = db.Customers.FirstOrDefault(c => c.Id == id);
35+
36+
// Return 404 Not Found if customer doesn't exist
37+
if (customer == null)
38+
{
39+
return NotFound();
40+
}
41+
42+
patchDoc.ApplyTo(customer, jsonPatchError =>
43+
{
44+
var key = jsonPatchError.AffectedObject.GetType().Name;
45+
ModelState.AddModelError(key, jsonPatchError.ErrorMessage);
46+
}
47+
);
48+
49+
if (!ModelState.IsValid)
50+
{
51+
return BadRequest(ModelState);
52+
}
53+
54+
return new ObjectResult(customer);
55+
}
56+
// </snippet_PatchAction>
57+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Microsoft.AspNetCore.Http.HttpResults;
2+
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
3+
using Microsoft.EntityFrameworkCore;
4+
5+
using App.Data;
6+
using App.Models;
7+
8+
internal static class CustomerApi {
9+
public static void MapCustomerApi(this IEndpointRouteBuilder routes)
10+
{
11+
var group = routes.MapGroup("/customers").WithTags("Customers");
12+
13+
group.MapGet("/{id}", async Task<Results<Ok<Customer>, NotFound>> (AppDb db, string id) =>
14+
{
15+
return await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id) is Customer customer
16+
? TypedResults.Ok(customer)
17+
: TypedResults.NotFound();
18+
});
19+
20+
group.MapPatch("/{id}", async Task<Results<Ok<Customer>,NotFound,BadRequest, ValidationProblem>> (AppDb db, string id,
21+
JsonPatchDocument<Customer> patchDoc) =>
22+
{
23+
var customer = await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id);
24+
if (customer is null)
25+
{
26+
return TypedResults.NotFound();
27+
}
28+
if (patchDoc != null)
29+
{
30+
Dictionary<string, string[]>? errors = null;
31+
patchDoc.ApplyTo(customer, jsonPatchError =>
32+
{
33+
errors ??= new ();
34+
var key = jsonPatchError.AffectedObject.GetType().Name;
35+
if (!errors.ContainsKey(key))
36+
{
37+
errors.Add(key, new string[] { });
38+
}
39+
errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
40+
});
41+
if (errors != null)
42+
{
43+
return TypedResults.ValidationProblem(errors);
44+
}
45+
await db.SaveChangesAsync();
46+
}
47+
48+
return TypedResults.Ok(customer);
49+
})
50+
.Accepts<JsonPatchDocument<Customer>>("application/json-patch+json");
51+
}
52+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using App.Models;
3+
4+
namespace App.Data;
5+
6+
public class AppDb : DbContext
7+
{
8+
public required DbSet<Customer> Customers { get; set; }
9+
10+
public AppDb(DbContextOptions<AppDb> options) : base(options)
11+
{
12+
}
13+
14+
protected override void OnModelCreating(ModelBuilder modelBuilder)
15+
{
16+
base.OnModelCreating(modelBuilder);
17+
18+
// Configure entity relationships here if needed
19+
modelBuilder.Entity<Customer>()
20+
.HasKey(c => c.Id);
21+
}
22+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using App.Models;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace App.Data;
5+
6+
public static class AppDbSeeder
7+
{
8+
public static async Task Seed(WebApplication app)
9+
{
10+
// Create and seed the database
11+
using (var scope = app.Services.CreateScope())
12+
{
13+
var services = scope.ServiceProvider;
14+
var context = services.GetRequiredService<AppDb>();
15+
context.Database.EnsureCreated();
16+
17+
if (context.Customers.Any())
18+
{
19+
return;
20+
}
21+
22+
Customer[] customers = {
23+
new Customer
24+
{
25+
Id = "1",
26+
Name = "John Doe",
27+
Email = "[email protected]",
28+
PhoneNumber = "555-123-4567",
29+
Address = "123 Main St, Anytown, USA"
30+
},
31+
new Customer
32+
{
33+
Id = "2",
34+
Name = "Jane Smith",
35+
Email = "[email protected]",
36+
PhoneNumber = "555-987-6543",
37+
Address = "456 Oak Ave, Somewhere, USA"
38+
},
39+
new Customer
40+
{
41+
Id = "3",
42+
Name = "Bob Johnson",
43+
Email = "[email protected]",
44+
PhoneNumber = "555-555-5555",
45+
Address = "789 Pine Rd, Elsewhere, USA"
46+
}
47+
};
48+
49+
await context.Customers.AddRangeAsync(customers);
50+
await context.SaveChangesAsync();
51+
}
52+
}
53+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.AspNetCore.JsonPatch.SystemTextJson" Version="10.0.0-preview.5.25277.114" />
11+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.5.25277.114" />
12+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0-preview.5.25277.114" />
13+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0-preview.5.25277.114" />
14+
</ItemGroup>
15+
16+
</Project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
@HostAddress = http://localhost:5221
2+
3+
GET {{HostAddress}}/openapi/v1.json
4+
Accept: application/json
5+
6+
###
7+
8+
GET {{HostAddress}}/api/customers/1
9+
Accept: application/json
10+
11+
###
12+
13+
PATCH {{HostAddress}}/api/customers/1
14+
Content-Type: application/json-patch+json
15+
Accept: application/json
16+
17+
[
18+
{
19+
"op": "replace",
20+
"path": "/email",
21+
"value": "[email protected]"
22+
}
23+
]
24+
25+
###
26+
27+
# Error response
28+
29+
PATCH {{HostAddress}}/api/customers/1
30+
Content-Type: application/json-patch+json
31+
Accept: application/json
32+
33+
[
34+
{
35+
"op": "add",
36+
"path": "/foobar",
37+
"value": 42
38+
}
39+
]
40+
41+
###
42+
### Minimal API requests
43+
###
44+
45+
GET {{HostAddress}}/customers/1
46+
Accept: application/json
47+
48+
###
49+
50+
PATCH {{HostAddress}}/customers/1
51+
Content-Type: application/json-patch+json
52+
Accept: application/json
53+
54+
[
55+
{
56+
"op": "replace",
57+
"path": "/email",
58+
"value": "[email protected]"
59+
}
60+
]
61+
62+
###
63+
64+
# Error response
65+
66+
PATCH {{HostAddress}}/customers/1
67+
Content-Type: application/json-patch+json
68+
Accept: application/json
69+
70+
[
71+
{
72+
"op": "add",
73+
"path": "/foobar",
74+
"value": 42
75+
}
76+
]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace App.Models;
2+
3+
public class Customer
4+
{
5+
public string Id { get; set; }
6+
public string? Name { get; set; }
7+
public string? Email { get; set; }
8+
public string? PhoneNumber { get; set; }
9+
public string? Address { get; set; }
10+
public List<Order>? Orders { get; set; }
11+
12+
public Customer()
13+
{
14+
Id = Guid.NewGuid().ToString();
15+
}
16+
}

0 commit comments

Comments
 (0)