diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13d67d3c6c..24b47eb237 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,9 @@ on: tags: - '*.*.*' permissions: + id-token: write contents: read + checks: write jobs: build-windows: @@ -17,12 +19,20 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Azure Login via OIDC + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Setup dotnet uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x 9.0.x + - name: Install NuGetKeyVaultSignTool + run: dotnet tool install --global NuGetKeyVaultSignTool - name: Build and Test run: | dotnet build --configuration Release @@ -32,25 +42,16 @@ jobs: dotnet test --configuration Release --no-build --results-directory ".\artifacts" -l trx .\src\AutoMapper.DI.Tests shell: pwsh - build: - needs: build-windows - strategy: - fail-fast: false - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Build and Test - run: ./Build.ps1 + - name: Generate SBOM + run: | + dotnet tool install --global Microsoft.Sbom.DotNetTool --version 4.1.5 + sbom-tool generate -b artifacts -bc src/AutoMapper -pn AutoMapper -pv ${{ github.ref_name }} -ps LuckyPennySoftware -nsb https://automapper.io/sbom shell: pwsh + - name: Sign packages + run: |- + foreach ($f in Get-ChildItem "./artifacts" -Filter "*.nupkg") { + 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 }} + } - name: Push to MyGet env: NUGET_URL: https://f.feedz.io/lucky-penny-software/automapper/nuget/index.json @@ -67,4 +68,23 @@ jobs: uses: actions/upload-artifact@v4 with: name: artifacts - path: artifacts/**/* \ No newline at end of file + path: artifacts/**/* + build: + needs: build-windows + strategy: + fail-fast: false + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + - name: Build and Test + run: ./Build.ps1 + shell: pwsh \ No newline at end of file diff --git a/docs/source/5.0-Upgrade-Guide.md b/docs/source/5.0-Upgrade-Guide.md index da377b1b09..6652bc8bd3 100644 --- a/docs/source/5.0-Upgrade-Guide.md +++ b/docs/source/5.0-Upgrade-Guide.md @@ -84,7 +84,7 @@ cfg.CreateMap().MaxDepth(3); cfg.CreateMap().PreserveReferences(); ``` -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. +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. ## UseDestinationValue diff --git a/docs/source/Configuration.md b/docs/source/Configuration.md index d501a57bd7..1e3b617e54 100644 --- a/docs/source/Configuration.md +++ b/docs/source/Configuration.md @@ -224,4 +224,20 @@ Compilation times increase with the size of the execution plan and that depends You can set `MapAtRuntime` per member or `MaxExecutionPlanDepth` globally (the default is one, set it to zero). 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. -Avoiding `PreserveReferences` and `MaxDepth` also helps. \ No newline at end of file +Avoiding `PreserveReferences` and `MaxDepth` also helps. + +## Circular and Self-Referential Types + +When AutoMapper detects a self-referential type mapping (e.g., `CreateMap()` 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). + +If your object graphs legitimately exceed 64 levels, increase the limit explicitly: + +```c# +cfg.CreateMap().MaxDepth(128); +``` + +To disable the depth limit entirely and rely solely on object-identity caching, call `.PreserveReferences()` explicitly: + +```c# +cfg.CreateMap().PreserveReferences(); +``` diff --git a/src/AutoMapper/Execution/TypeMapPlanBuilder.cs b/src/AutoMapper/Execution/TypeMapPlanBuilder.cs index 5fe693bf8e..6bfe5afd0b 100644 --- a/src/AutoMapper/Execution/TypeMapPlanBuilder.cs +++ b/src/AutoMapper/Execution/TypeMapPlanBuilder.cs @@ -135,6 +135,10 @@ private static void CheckForCycles(IGlobalConfiguration configuration, TypeMap t } memberTypeMap.PreserveReferences = true; + if (memberTypeMap.MaxDepth == 0) + { + memberTypeMap.MaxDepth = 64; + } Trace(typeMap, memberTypeMap, memberMap); if (memberMap.Inline) { diff --git a/src/UnitTests/Bug/DeepNestingStackOverflow.cs b/src/UnitTests/Bug/DeepNestingStackOverflow.cs new file mode 100644 index 0000000000..1a1a7a5a46 --- /dev/null +++ b/src/UnitTests/Bug/DeepNestingStackOverflow.cs @@ -0,0 +1,47 @@ +namespace AutoMapper.UnitTests.Bug; + +public class DeepNestingStackOverflow +{ + class Circular { public Circular Self { get; set; } } + + // Verifies that mapping a deeply nested self-referential object does not + // crash the process with a StackOverflowException (GHSA-rvv3-g6hj-g44x). + // AutoMapper auto-applies a default MaxDepth of 64 (matching System.Text.Json + // and Newtonsoft.Json) when it detects a self-referential type mapping. + [Fact] + public void Mapping_deeply_nested_self_referential_object_should_not_stackoverflow() + { + var config = new MapperConfiguration(cfg => cfg.CreateMap()); + var mapper = config.CreateMapper(); + + var root = new Circular(); + var current = root; + for (int i = 0; i < 30_000; i++) + { + current.Self = new Circular(); + current = current.Self; + } + + // Should complete without crashing; mapping is truncated at default MaxDepth (64) + var result = mapper.Map(root); + result.ShouldNotBeNull(); + + int depth = 0; + current = result; + while (current.Self != null) + { + depth++; + current = current.Self; + } + depth.ShouldBeLessThanOrEqualTo(64); + } + + // Verifies that configuration validation does not detect the vulnerability — + // only the runtime mapping is affected, not the configuration itself. + [Fact] + public void AssertConfigurationIsValid_does_not_detect_deep_nesting_vulnerability() + { + var config = new MapperConfiguration(cfg => cfg.CreateMap()); + config.AssertConfigurationIsValid(); + } +} diff --git a/src/UnitTests/Bug/MultiThreadingIssues.cs b/src/UnitTests/Bug/MultiThreadingIssues.cs index 8bbf0e3145..9b964efab6 100644 --- a/src/UnitTests/Bug/MultiThreadingIssues.cs +++ b/src/UnitTests/Bug/MultiThreadingIssues.cs @@ -641,7 +641,7 @@ public async Task Should_work() }; var tasks = types - .Concat(types.Select(t => t.Reverse().ToArray())) + .Concat(types.Select(t => t.AsEnumerable().Reverse().ToArray())) .Select(t=>(SourceType: sourceType.MakeGenericType(t[0]), DestinationType: destinationType.MakeGenericType(t[1]))) .ToArray() .Select(s => Task.Factory.StartNew(() => c.ResolveTypeMap(s.SourceType, s.DestinationType))) @@ -1173,7 +1173,7 @@ public async Task Should_work() }; var tasks = types - .Concat(types.Select(t => t.Reverse().ToArray())) + .Concat(types.Select(t => t.AsEnumerable().Reverse().ToArray())) .Select(t=>(SourceType: sourceType.MakeGenericType(t[0]), DestinationType: destinationType.MakeGenericType(t[1]))) .ToArray() .Select(s => Task.Factory.StartNew(() => mapper.Map(null, s.SourceType, s.DestinationType)))