From 4d76ac1937f5efa1d9061da786e1e97d3963d1e1 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Fri, 14 Aug 2026 23:08:27 +0200 Subject: [PATCH 1/4] Clarify the security note: cover both packages and surface it at the top of the README The name-based subtype resolution risk applied to Newtonsoft.Json too, but only the System.Text.Json section mentioned it, buried at the bottom. Promote it to a dedicated section right after the intro, state the exact trigger (no [KnownSubType]/builder mapping declared), and point the STJ section at it. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0924267..af0577e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ __JsonSubTypes__ is a discriminated Json sub-type Converter implementation for . > **Note:** this library is built around `Json.NET`/`Newtonsoft.Json` — that is where its API and reputation come from, and the `JsonSubTypes` NuGet package targets it. A `System.Text.Json` port exists as the `JsonSubTypes.Text.Json` package (`.NET 8+`): it shares the same API but is **experimental**. Full documentation, differences and known limitations are in the dedicated section at the bottom: [System.Text.Json variant](#systemtextjson-variant). +## Security + +When a subtype is resolved by *name* — which happens for both packages only when no `[KnownSubType]`/builder mapping is declared at all — the converter turns the JSON discriminator string into a type name and instantiates the matching type. Only types assignable from the polymorphic base type can be resolved, but any such type present in the base type's assembly (for Newtonsoft.Json) or in that assembly plus any assembly registered via `JsonSubTypesTypeResolution` (for `System.Text.Json`) can be instantiated with attacker-controlled JSON. Do **not** expose a name-based hierarchy to untrusted JSON without validating the payload upstream; prefer explicit `[KnownSubType]` or builder mappings whenever the discriminator can come from outside your own code. ## DeserializeObject with custom type property name @@ -363,7 +366,7 @@ public interface IExpression { } - Dotted or nested discriminator property paths (e.g. `"nested.property"`) are supported. - **Fallback paths**: serializing the base type itself (rather than a subtype) and deserializing an unknown discriminator back to the base use a reflection-based writer/reader, because the base type's contract is owned by the converter (`System.Text.Json` exposes no property metadata for converter-owned types). `[JsonPropertyName]`, `[JsonIgnore]` (including `JsonIgnoreCondition`), the naming policy and `DefaultIgnoreCondition` are honored; per-property `[JsonConverter]`, `[JsonInclude]` fields, `required` members and parameterized constructors are not supported on these two paths. - **Performance**: writing an object with a discriminator serializes it once, then re-parses the JSON (`JsonDocument`) to inject the discriminator property, so payloads spend roughly 2-3x their size in temporary memory on the write path. This is the cost of the converter architecture and of the `MaxDepth + 1` note above. -- **Security**: name-based subtype resolution (`GetTypeByName`, used when no `[KnownSubType]` mapping is declared) resolves a type name from the JSON discriminator against the base type's assembly (and any assembly registered via `JsonSubTypesTypeResolution`). Only types assignable from the base can be resolved, but do **not** expose a name-based hierarchy to untrusted JSON without validating the payload upstream. +- **Security**: see the [security note at the top of this README](#security). It applies to both packages; the only difference is the set of assemblies searched for a name-based hit. - The property-presence builder (`JsonSubtypesWithPropertyConverterBuilder`) registers subtypes by property name, so two subtypes cannot share the same property name through the builder (use `[KnownSubTypeWithProperty]` attributes for that case). ### Which engine should I use? From f3db6ff3b443f7988817815c2af944ace6a3e69b Mon Sep 17 00:00:00 2001 From: manuc66 Date: Fri, 14 Aug 2026 23:23:25 +0200 Subject: [PATCH 2/4] Document the name-based resolution security risk on both converters Explicit [KnownSubType] mapping is the safe path; document the exact trigger (no mapping declared), the assembly scope and the assignability filter for Newtonsoft.Json and System.Text.Json so the risk is visible in the IDE, not only in the README. --- JsonSubTypes.Text.Json/JsonSubtypes.cs | 19 +++++++++++++++++++ JsonSubTypes/JsonSubtypes.cs | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/JsonSubTypes.Text.Json/JsonSubtypes.cs b/JsonSubTypes.Text.Json/JsonSubtypes.cs index 97b50c6..870c211 100644 --- a/JsonSubTypes.Text.Json/JsonSubtypes.cs +++ b/JsonSubTypes.Text.Json/JsonSubtypes.cs @@ -95,6 +95,25 @@ internal interface IJsonSubtypes [RequiresUnreferencedCode("JsonSubtypes uses reflection to discover sub-types and properties.")] [RequiresDynamicCode("JsonSubtypes requires dynamic code for runtime type creation.")] +/// +/// A JSON converter that deserializes a polymorphic hierarchy from a discriminator property. +/// The concrete subtype is resolved from an explicit mapping () +/// or, when no mapping is declared, by matching the discriminator string against a type name. +/// +/// +/// +/// Name-based resolution (used only when no mapping is +/// declared) instantiates the type whose name matches the discriminator, provided it is +/// assignable from the polymorphic base type and lives in the base type's assembly or in an +/// assembly registered via . Any such type present in +/// those assemblies can be instantiated with attacker-controlled JSON. +/// +/// +/// Do not expose a name-based hierarchy to untrusted JSON without validating the payload +/// upstream; prefer an explicit mapping whenever the +/// discriminator can come from outside your own code. +/// +/// public class JsonSubtypes : JsonConverter, IJsonSubtypes where T : class { private static readonly ConcurrentDictionary> diff --git a/JsonSubTypes/JsonSubtypes.cs b/JsonSubTypes/JsonSubtypes.cs index 0e25612..1069506 100644 --- a/JsonSubTypes/JsonSubtypes.cs +++ b/JsonSubTypes/JsonSubtypes.cs @@ -38,6 +38,24 @@ namespace JsonSubTypes // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. + /// + /// A JSON converter that deserializes a polymorphic hierarchy from a discriminator property. + /// The concrete subtype is resolved from an explicit mapping () + /// or, when no mapping is declared, by matching the discriminator string against a type name. + /// + /// + /// + /// Name-based resolution (used only when no mapping is + /// declared) instantiates the type whose name matches the discriminator, provided it is + /// assignable from the polymorphic base type and lives in the base type's assembly. Any such + /// type present in that assembly can be instantiated with attacker-controlled JSON. + /// + /// + /// Do not expose a name-based hierarchy to untrusted JSON without validating the payload + /// upstream; prefer an explicit mapping whenever the + /// discriminator can come from outside your own code. + /// + /// public class JsonSubtypes : JsonConverter { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)] From c87e7b2cc439681f273764739dc89b2ddc947082 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Fri, 14 Aug 2026 23:34:53 +0200 Subject: [PATCH 3/4] Re-balance the security note: short pointer up top, details back in the STJ section The dedicated top section was too prominent for a documented risk with a real mitigation. Keep a one-line pointer near the intro and move the full analysis back next to the other STJ differences, stressing that name-based resolution only runs when no subtype mapping is declared at all. --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index af0577e..27424b7 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,8 @@ __JsonSubTypes__ is a discriminated Json sub-type Converter implementation for . [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmanuc66%2FJsonSubTypes.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmanuc66%2FJsonSubTypes?ref=badge_shield) > **Note:** this library is built around `Json.NET`/`Newtonsoft.Json` — that is where its API and reputation come from, and the `JsonSubTypes` NuGet package targets it. A `System.Text.Json` port exists as the `JsonSubTypes.Text.Json` package (`.NET 8+`): it shares the same API but is **experimental**. Full documentation, differences and known limitations are in the dedicated section at the bottom: [System.Text.Json variant](#systemtextjson-variant). - -## Security - -When a subtype is resolved by *name* — which happens for both packages only when no `[KnownSubType]`/builder mapping is declared at all — the converter turns the JSON discriminator string into a type name and instantiates the matching type. Only types assignable from the polymorphic base type can be resolved, but any such type present in the base type's assembly (for Newtonsoft.Json) or in that assembly plus any assembly registered via `JsonSubTypesTypeResolution` (for `System.Text.Json`) can be instantiated with attacker-controlled JSON. Do **not** expose a name-based hierarchy to untrusted JSON without validating the payload upstream; prefer explicit `[KnownSubType]` or builder mappings whenever the discriminator can come from outside your own code. +> +> **Security:** unless a subtype mapping is explicitly declared, the converter resolves subtypes by *name* from the JSON discriminator (only types assignable from the base are considered). See the [security section](#security) before exposing a name-based hierarchy to untrusted JSON. ## DeserializeObject with custom type property name @@ -366,9 +364,15 @@ public interface IExpression { } - Dotted or nested discriminator property paths (e.g. `"nested.property"`) are supported. - **Fallback paths**: serializing the base type itself (rather than a subtype) and deserializing an unknown discriminator back to the base use a reflection-based writer/reader, because the base type's contract is owned by the converter (`System.Text.Json` exposes no property metadata for converter-owned types). `[JsonPropertyName]`, `[JsonIgnore]` (including `JsonIgnoreCondition`), the naming policy and `DefaultIgnoreCondition` are honored; per-property `[JsonConverter]`, `[JsonInclude]` fields, `required` members and parameterized constructors are not supported on these two paths. - **Performance**: writing an object with a discriminator serializes it once, then re-parses the JSON (`JsonDocument`) to inject the discriminator property, so payloads spend roughly 2-3x their size in temporary memory on the write path. This is the cost of the converter architecture and of the `MaxDepth + 1` note above. -- **Security**: see the [security note at the top of this README](#security). It applies to both packages; the only difference is the set of assemblies searched for a name-based hit. +- **Security**: see the [security section](#security) at the bottom of this section. It applies to both packages; the only difference is the set of assemblies searched for a name-based hit. - The property-presence builder (`JsonSubtypesWithPropertyConverterBuilder`) registers subtypes by property name, so two subtypes cannot share the same property name through the builder (use `[KnownSubTypeWithProperty]` attributes for that case). +### Security + +When a subtype is resolved by *name* — which happens for both packages **only when no subtype mapping is declared at all** (no `[KnownSubType]` attribute, no `RegisterSubtype` builder call) — the converter turns the JSON discriminator string into a type name and instantiates the matching type. Declaring a mapping at all switches the converter to that mapping, even when no entry matches; the name-based path is never used then. + +Only types assignable from the polymorphic base type can be resolved, but any such type present in the base type's assembly (for Newtonsoft.Json) or in that assembly plus any assembly registered via `JsonSubTypesTypeResolution` (for `System.Text.Json`) can be instantiated with attacker-controlled JSON. Do **not** expose a name-based hierarchy to untrusted JSON without validating the payload upstream; prefer explicit `[KnownSubType]` or builder mappings whenever the discriminator can come from outside your own code. + ### Which engine should I use? `JsonSubTypes.Text.Json` ships three engines that share the same configuration layer (the attributes and `JsonSubtypesConverterBuilder`), and a parity test battery keeps them aligned: From 68750fad9d7ea5d5bda3b33e07b7c99c77fccc11 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Fri, 14 Aug 2026 23:39:07 +0200 Subject: [PATCH 4/4] Open the README with a state-and-choices section instead of a bare note Introduce the two packages and the shared API first, tell the reader the examples are valid for both, and defer the engine choice to the STJ section where those engines are actually explained. --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27424b7..96d445e 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,15 @@ __JsonSubTypes__ is a discriminated Json sub-type Converter implementation for . [![CodeFactor](https://www.codefactor.io/repository/github/manuc66/JsonSubTypes/badge)](https://www.codefactor.io/repository/github/manuc66/JsonSubTypes) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmanuc66%2FJsonSubTypes.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmanuc66%2FJsonSubTypes?ref=badge_shield) -> **Note:** this library is built around `Json.NET`/`Newtonsoft.Json` — that is where its API and reputation come from, and the `JsonSubTypes` NuGet package targets it. A `System.Text.Json` port exists as the `JsonSubTypes.Text.Json` package (`.NET 8+`): it shares the same API but is **experimental**. Full documentation, differences and known limitations are in the dedicated section at the bottom: [System.Text.Json variant](#systemtextjson-variant). -> +## Which package? State and choices + +`JsonSubTypes` exists in two packages that share the same API and registration model (attributes and `JsonSubtypesConverterBuilder`): + +- **`JsonSubTypes`** — for `Newtonsoft.Json`, the original and stable package. +- **`JsonSubTypes.Text.Json`** (`.NET 8+`) — for `System.Text.Json`. **Experimental**: the API is complete and the code fully tested, but the stable `1.0.0` release is still pending. + +The examples below use the Newtonsoft.Json package; the API is the same for `System.Text.Json`, so read them either way. If you are targeting `System.Text.Json`, then after these examples jump to the [System.Text.Json variant](#systemtextjson-variant) section, which explains the engines available there (`Build()` converter, `BuildResolver()`, AOT generator) and their differences and limitations. + > **Security:** unless a subtype mapping is explicitly declared, the converter resolves subtypes by *name* from the JSON discriminator (only types assignable from the base are considered). See the [security section](#security) before exposing a name-based hierarchy to untrusted JSON. ## DeserializeObject with custom type property name