Skip to content

fix(serialization)!: enforce type allow-list in default JSON storage serializer - #10268

Merged
ReubenBond merged 1 commit into
dotnet:mainfrom
ReubenBond:reubenbond-secure-newtonsoft-json-defaults
Jul 31, 2026
Merged

ReubenBond merged 1 commit into
dotnet:mainfrom
ReubenBond:reubenbond-secure-newtonsoft-json-defaults

Conversation

@ReubenBond

@ReubenBond ReubenBond commented Jul 15, 2026

Copy link
Copy Markdown
Member

Problem

Orleans'' default grain-storage serializer (JsonGrainStorageSerializerOrleansJsonSerializer) is configured with TypeNameHandling.All, so every serialized object embeds a $type token that is honored on read. The OrleansJsonSerializationBinder resolved that $type via CachedTypeResolver and, on failure, fell back to Newtonsoft''s permissive DefaultSerializationBinderneither consults any allow-list. An attacker able to influence persisted, streamed, or transactional state could therefore cause arbitrary CLR types to be constructed during deserialization — the classic TypeNameHandling.All gadget-chain vulnerability. The same shared settings feed grain storage, JSON streaming, and (Azure) transactional state, so all three were affected.

Solution

Make the binder enforce Orleans'' existing type allow-list rather than introduce a parallel Newtonsoft-specific system. OrleansJsonSerializationBinder.BindToType now resolves $type through TypeConverter, which only constructs types that are permitted by the configured ITypeNameFilter / ITypeFilter / TypeManifestOptions.AllowedTypes (or auto-allow-listed via [GenerateSerializer]). Disallowed types throw an actionable JsonSerializationException that lists every remediation option.

This is secure by default with an opt-out:

  • Well-behaved apps keep working unchanged: [GenerateSerializer] state types are already auto-allow-listed, and DefaultTypeFilter still permits BCL collections/primitives. TypeNameHandling.All is retained so existing persisted state stays readable — security comes from the binder, not from changing the wire format.
  • A new, dedicated OrleansJsonSerializerOptions.AllowAllTypes (default false) restores the previous permissive behavior for the JSON storage/streaming/transaction path only, without weakening the native serializer''s global type filtering. Narrower opt-ins (AllowedTypes, ITypeNameFilter) are preferred and are surfaced in the exception message.
  • The legacy OrleansJsonSerializationBinder(TypeResolver) constructor is kept (permissive) for API compatibility; a new constructor takes the TypeConverter + opt-out flag.

Compatibility

Breaking for apps that persist $type entries for types that are neither [GenerateSerializer]-registered nor covered by a filter — these now throw on read. The thrown exception explains the four ways to resolve it: mark the type [GenerateSerializer], add it to TypeManifestOptions.AllowedTypes, register an ITypeNameFilter/ITypeFilter, or set OrleansJsonSerializerOptions.AllowAllTypes = true to restore the old behavior.

Microsoft Reviewers: Open in CodeFlow

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI review requested due to automatic review settings July 31, 2026 16:35
@ReubenBond
ReubenBond force-pushed the reubenbond-secure-newtonsoft-json-defaults branch from 44143cf to 8d48046 Compare July 31, 2026 16:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens Orleans’ default Newtonsoft.Json-based serializer path (grain storage/JSON streaming/transactional state) by enforcing the existing Orleans type allow-list when resolving $type values emitted under TypeNameHandling.All, mitigating gadget-chain deserialization risks while keeping the wire format compatible.

Changes:

  • Update OrleansJsonSerializationBinder to resolve types through TypeConverter (allow-list enforcement), with an opt-out flag for legacy permissive behavior.
  • Introduce OrleansJsonSerializerOptions.AllowAllTypes and plumb it through default JSON serializer settings configuration.
  • Add targeted tests validating allow-listed vs disallowed type resolution and round-trip behavior, plus update API surface.
Show a summary per file
File Description
test/Orleans.Core.Tests/Serialization/OrleansJsonSerializationBinderTests.cs Adds coverage for strict vs permissive binder behavior during type binding and deserialization.
src/Orleans.Core/Serialization/OrleansJsonSerializerSettings.cs Threads AllowAllTypes from options into binder creation for default JSON settings.
src/Orleans.Core/Serialization/OrleansJsonSerializerOptions.cs Adds AllowAllTypes option and ensures settings are configured accordingly.
src/Orleans.Core/Serialization/OrleansJsonSerializationBinder.cs Enforces Orleans type allow-list via TypeConverter, with legacy permissive compatibility path.
src/api/Orleans.Core/Orleans.Core.cs Updates public API surface for new constructor and options property.

Copilot's findings

Suppressed comments (1)

test/Orleans.Core.Tests/Serialization/OrleansJsonSerializationBinderTests.cs:126

  • This assertion uses Assert.ThrowsAny, which is overly broad and could pass for unrelated failures. Since deserialization of a disallowed $type is expected to fail with JsonSerializationException (thrown by the binder), assert that exception type explicitly.
        Assert.ThrowsAny<Exception>(() => JsonConvert.DeserializeObject(json, typeof(object), settings));
  • Files reviewed: 5/5 changed files
  • Comments generated: 2

Comment thread src/Orleans.Core/Serialization/OrleansJsonSerializationBinder.cs
Comment thread test/Orleans.Core.Tests/Serialization/OrleansJsonSerializationBinderTests.cs Outdated
…erializer

The default grain-storage serializer (`JsonGrainStorageSerializer` ->
`OrleansJsonSerializer`) is configured with `TypeNameHandling.All`, and its
`OrleansJsonSerializationBinder` resolved any type named in the payload's
`$type` token (via `CachedTypeResolver` and Newtonsoft's permissive
`DefaultSerializationBinder`). This let an attacker who can influence
persisted/streamed/transactional state cause arbitrary CLR types to be
constructed during deserialization (a classic `TypeNameHandling.All`
gadget-chain vulnerability).

Make the binder enforce Orleans' existing type allow-list (reusing
`TypeConverter` / `ITypeNameFilter` / `TypeManifestOptions.AllowedTypes`)
instead of building a parallel Newtonsoft-specific mechanism. Types marked
`[GenerateSerializer]` are already auto-allow-listed and keep working with no
changes; BCL collections/primitives remain permitted by `DefaultTypeFilter`.
Disallowed `$type` tokens now throw an actionable `JsonSerializationException`.

This is secure by default with an opt-out: the new
`OrleansJsonSerializerOptions.AllowAllTypes` (default false) restores the
previous permissive behavior for the JSON storage/streaming/transaction path
only, without weakening the native serializer's global filtering. The legacy
`OrleansJsonSerializationBinder(TypeResolver)` constructor is retained
(permissive) for API compatibility.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 75c9fe25-7322-4eb8-b0d4-a475959d5658
Copilot AI review requested due to automatic review settings July 31, 2026 17:30
@ReubenBond
ReubenBond force-pushed the reubenbond-secure-newtonsoft-json-defaults branch from 8d48046 to 2494046 Compare July 31, 2026 17:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

Suppressed comments (1)

src/Orleans.Core/Serialization/OrleansJsonSerializationBinder.cs:33

  • The legacy constructor does not assign _typeConverter, but _typeConverter is a readonly field. This will not compile (readonly fields must be definitely assigned in every constructor).
        public OrleansJsonSerializationBinder(TypeResolver typeResolver)
        {
            _typeResolver = typeResolver;
            _allowAllTypes = true;
        }
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new

@ReubenBond ReubenBond changed the title fix(serialization): enforce type allow-list in default JSON storage serializer fix(serialization)!: enforce type allow-list in default JSON storage serializer Jul 31, 2026
@ReubenBond
ReubenBond merged commit 54f2fc0 into dotnet:main Jul 31, 2026
67 checks passed
@ReubenBond
ReubenBond deleted the reubenbond-secure-newtonsoft-json-defaults branch July 31, 2026 19:52
This was referenced Aug 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants