Skip to content

Commit 8a441c6

Browse files
authored
Merge pull request #4619 from LuckyPennySoftware/security/GHSA-rvv3-g6hj-g44x-15.x
Fix GHSA-rvv3-g6hj-g44x: default MaxDepth of 64 for self-referential types (15.x backport)
2 parents 7ae9525 + c2a9521 commit 8a441c6

File tree

6 files changed

+110
-23
lines changed

6 files changed

+110
-23
lines changed

.github/workflows/release.yml

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ on:
55
tags:
66
- '*.*.*'
77
permissions:
8+
id-token: write
89
contents: read
10+
checks: write
911

1012
jobs:
1113
build-windows:
@@ -17,12 +19,20 @@ jobs:
1719
uses: actions/checkout@v4
1820
with:
1921
fetch-depth: 0
22+
- name: Azure Login via OIDC
23+
uses: azure/login@v2
24+
with:
25+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
26+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
27+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
2028
- name: Setup dotnet
2129
uses: actions/setup-dotnet@v4
2230
with:
2331
dotnet-version: |
2432
8.0.x
2533
9.0.x
34+
- name: Install NuGetKeyVaultSignTool
35+
run: dotnet tool install --global NuGetKeyVaultSignTool
2636
- name: Build and Test
2737
run: |
2838
dotnet build --configuration Release
@@ -32,25 +42,16 @@ jobs:
3242
dotnet test --configuration Release --no-build --results-directory ".\artifacts" -l trx .\src\AutoMapper.DI.Tests
3343
3444
shell: pwsh
35-
build:
36-
needs: build-windows
37-
strategy:
38-
fail-fast: false
39-
runs-on: ubuntu-latest
40-
steps:
41-
- name: Checkout
42-
uses: actions/checkout@v4
43-
with:
44-
fetch-depth: 0
45-
- name: Setup dotnet
46-
uses: actions/setup-dotnet@v4
47-
with:
48-
dotnet-version: |
49-
8.0.x
50-
9.0.x
51-
- name: Build and Test
52-
run: ./Build.ps1
45+
- name: Generate SBOM
46+
run: |
47+
dotnet tool install --global Microsoft.Sbom.DotNetTool --version 4.1.5
48+
sbom-tool generate -b artifacts -bc src/AutoMapper -pn AutoMapper -pv ${{ github.ref_name }} -ps LuckyPennySoftware -nsb https://automapper.io/sbom
5349
shell: pwsh
50+
- name: Sign packages
51+
run: |-
52+
foreach ($f in Get-ChildItem "./artifacts" -Filter "*.nupkg") {
53+
NuGetKeyVaultSignTool sign $f.FullName --file-digest sha256 --timestamp-rfc3161 http://timestamp.digicert.com --azure-key-vault-managed-identity --azure-key-vault-url ${{ secrets.AZURE_KEYVAULT_URI }} --azure-key-vault-certificate ${{ secrets.CODESIGN_CERT_NAME }}
54+
}
5455
- name: Push to MyGet
5556
env:
5657
NUGET_URL: https://f.feedz.io/lucky-penny-software/automapper/nuget/index.json
@@ -67,4 +68,23 @@ jobs:
6768
uses: actions/upload-artifact@v4
6869
with:
6970
name: artifacts
70-
path: artifacts/**/*
71+
path: artifacts/**/*
72+
build:
73+
needs: build-windows
74+
strategy:
75+
fail-fast: false
76+
runs-on: ubuntu-latest
77+
steps:
78+
- name: Checkout
79+
uses: actions/checkout@v4
80+
with:
81+
fetch-depth: 0
82+
- name: Setup dotnet
83+
uses: actions/setup-dotnet@v4
84+
with:
85+
dotnet-version: |
86+
8.0.x
87+
9.0.x
88+
- name: Build and Test
89+
run: ./Build.ps1
90+
shell: pwsh

docs/source/5.0-Upgrade-Guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ cfg.CreateMap<Category, CategoryDto>().MaxDepth(3);
8484
cfg.CreateMap<User, UserDto>().PreserveReferences();
8585
```
8686

87-
Starting from 6.1.0 PreserveReferences is set automatically at config time whenever the recursion can be detected statically. If you're still getting `StackOverflowException`, open an issue with a full repro and we'll look into it.
87+
Starting from 6.1.0 PreserveReferences is set automatically at config time whenever the recursion can be detected statically. Starting from 15.1.1, a default MaxDepth of 64 is also applied automatically, preventing a StackOverflowException from deeply nested (but non-circular) object graphs (see GHSA-rvv3-g6hj-g44x). If you need deeper mapping, call `.MaxDepth(n)` explicitly. To rely solely on object-identity caching without a depth limit, call `.PreserveReferences()` explicitly.
8888

8989
## UseDestinationValue
9090

docs/source/Configuration.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,20 @@ Compilation times increase with the size of the execution plan and that depends
224224
You can set `MapAtRuntime` per member or `MaxExecutionPlanDepth` globally (the default is one, set it to zero).
225225

226226
These will reduce the size of the execution plan by replacing the execution plan for a child object with a method call. The compilation will be faster, but the mapping itself might be slower. Search the repo for more details and use a profiler to better understand the effect.
227-
Avoiding `PreserveReferences` and `MaxDepth` also helps.
227+
Avoiding `PreserveReferences` and `MaxDepth` also helps.
228+
229+
## Circular and Self-Referential Types
230+
231+
When AutoMapper detects a self-referential type mapping (e.g., `CreateMap<Node, Node>()` where `Node` has a `Node` property), it automatically enables `PreserveReferences` to avoid re-mapping the same object instance. It also applies a default `MaxDepth` of **64** — matching System.Text.Json and Newtonsoft.Json — to prevent a Denial-of-Service condition from deeply nested object graphs (see GHSA-rvv3-g6hj-g44x).
232+
233+
If your object graphs legitimately exceed 64 levels, increase the limit explicitly:
234+
235+
```c#
236+
cfg.CreateMap<Node, Node>().MaxDepth(128);
237+
```
238+
239+
To disable the depth limit entirely and rely solely on object-identity caching, call `.PreserveReferences()` explicitly:
240+
241+
```c#
242+
cfg.CreateMap<Node, Node>().PreserveReferences();
243+
```

src/AutoMapper/Execution/TypeMapPlanBuilder.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ private static void CheckForCycles(IGlobalConfiguration configuration, TypeMap t
135135
}
136136

137137
memberTypeMap.PreserveReferences = true;
138+
if (memberTypeMap.MaxDepth == 0)
139+
{
140+
memberTypeMap.MaxDepth = 64;
141+
}
138142
Trace(typeMap, memberTypeMap, memberMap);
139143
if (memberMap.Inline)
140144
{
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
namespace AutoMapper.UnitTests.Bug;
2+
3+
public class DeepNestingStackOverflow
4+
{
5+
class Circular { public Circular Self { get; set; } }
6+
7+
// Verifies that mapping a deeply nested self-referential object does not
8+
// crash the process with a StackOverflowException (GHSA-rvv3-g6hj-g44x).
9+
// AutoMapper auto-applies a default MaxDepth of 64 (matching System.Text.Json
10+
// and Newtonsoft.Json) when it detects a self-referential type mapping.
11+
[Fact]
12+
public void Mapping_deeply_nested_self_referential_object_should_not_stackoverflow()
13+
{
14+
var config = new MapperConfiguration(cfg => cfg.CreateMap<Circular, Circular>());
15+
var mapper = config.CreateMapper();
16+
17+
var root = new Circular();
18+
var current = root;
19+
for (int i = 0; i < 30_000; i++)
20+
{
21+
current.Self = new Circular();
22+
current = current.Self;
23+
}
24+
25+
// Should complete without crashing; mapping is truncated at default MaxDepth (64)
26+
var result = mapper.Map<Circular>(root);
27+
result.ShouldNotBeNull();
28+
29+
int depth = 0;
30+
current = result;
31+
while (current.Self != null)
32+
{
33+
depth++;
34+
current = current.Self;
35+
}
36+
depth.ShouldBeLessThanOrEqualTo(64);
37+
}
38+
39+
// Verifies that configuration validation does not detect the vulnerability —
40+
// only the runtime mapping is affected, not the configuration itself.
41+
[Fact]
42+
public void AssertConfigurationIsValid_does_not_detect_deep_nesting_vulnerability()
43+
{
44+
var config = new MapperConfiguration(cfg => cfg.CreateMap<Circular, Circular>());
45+
config.AssertConfigurationIsValid();
46+
}
47+
}

src/UnitTests/Bug/MultiThreadingIssues.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ public async Task Should_work()
641641
};
642642
var tasks =
643643
types
644-
.Concat(types.Select(t => t.Reverse().ToArray()))
644+
.Concat(types.Select(t => t.AsEnumerable().Reverse().ToArray()))
645645
.Select(t=>(SourceType: sourceType.MakeGenericType(t[0]), DestinationType: destinationType.MakeGenericType(t[1])))
646646
.ToArray()
647647
.Select(s => Task.Factory.StartNew(() => c.ResolveTypeMap(s.SourceType, s.DestinationType)))
@@ -1173,7 +1173,7 @@ public async Task Should_work()
11731173
};
11741174
var tasks =
11751175
types
1176-
.Concat(types.Select(t => t.Reverse().ToArray()))
1176+
.Concat(types.Select(t => t.AsEnumerable().Reverse().ToArray()))
11771177
.Select(t=>(SourceType: sourceType.MakeGenericType(t[0]), DestinationType: destinationType.MakeGenericType(t[1])))
11781178
.ToArray()
11791179
.Select(s => Task.Factory.StartNew(() => mapper.Map(null, s.SourceType, s.DestinationType)))

0 commit comments

Comments
 (0)