diff --git a/.gitignore b/.gitignore
index e76fe3cb..4831b9e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -145,3 +145,5 @@ $RECYCLE.BIN/
.DS_Store
*.nupkg
*.binlog
+src/snk
+src/Test/Desktop/OpenRiaServices.Common.DomainServices.Test/DataModels/ScenarioModels/northwind.map
diff --git a/Changelog.md b/Changelog.md
index 9beed361..fc69460b 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,3 +1,13 @@
+# AspNetCore 1.5.0
+
+* Allow configuring Serializer security settings
+
+You can configure reader quotas to limit resource consumption and mitigate denial-of-service (DoS) attacks.
+By default all quotas are set to their maximum values to preserve backward compatibility.
+
+See [AspNetCore README](src/OpenRiaServices.Hosting.AspNetCore/Framework/README.md#configuring-serializer-settings) for more details and sample code.
+
+
# EF Core 4.1.0
* Generate `[Required(AllowEmptyStrings=true)]` instead of `[Required]` on client for non nullable string properties
diff --git a/src/OpenRiaServices.Client.DomainClients.Http/Framework/BinaryHttpDomainClientFactory.cs b/src/OpenRiaServices.Client.DomainClients.Http/Framework/BinaryHttpDomainClientFactory.cs
index 36dd4c57..6acab569 100644
--- a/src/OpenRiaServices.Client.DomainClients.Http/Framework/BinaryHttpDomainClientFactory.cs
+++ b/src/OpenRiaServices.Client.DomainClients.Http/Framework/BinaryHttpDomainClientFactory.cs
@@ -1,5 +1,6 @@
-using System;
+using System;
using System.Net.Http;
+using System.Xml;
using OpenRiaServices.Client.DomainClients.Http;
namespace OpenRiaServices.Client.DomainClients
@@ -11,6 +12,16 @@ namespace OpenRiaServices.Client.DomainClients
public class BinaryHttpDomainClientFactory
: HttpDomainClientFactory
{
+ ///
+ /// Gets or sets the quotas used by when reading responses.
+ ///
+ public XmlDictionaryReaderQuotas ReaderQuotas { get; set; } = CreateMaxQuotas();
+
+ ///
+ /// Gets or sets the shared dictionary used for binary XML reader/writer compression.
+ ///
+ public IXmlDictionary Dictionary { get; set; }
+
///
public BinaryHttpDomainClientFactory(Uri serverBaseUri, HttpMessageHandler messageHandler)
: base(serverBaseUri, messageHandler)
@@ -34,7 +45,14 @@ protected override DomainClient CreateDomainClientCore(Type serviceContract, Uri
{
HttpClient httpClient = CreateHttpClient(serviceUri, BinaryHttpDomainClient.MediaType);
- return new BinaryHttpDomainClient(httpClient, serviceContract);
+ return new BinaryHttpDomainClient(httpClient, serviceContract, ReaderQuotas, Dictionary);
+ }
+
+ private static XmlDictionaryReaderQuotas CreateMaxQuotas()
+ {
+ var quotas = new XmlDictionaryReaderQuotas();
+ XmlDictionaryReaderQuotas.Max.CopyTo(quotas);
+ return quotas;
}
}
}
diff --git a/src/OpenRiaServices.Client.DomainClients.Http/Framework/Http/BinaryHttpDomainClient.cs b/src/OpenRiaServices.Client.DomainClients.Http/Framework/Http/BinaryHttpDomainClient.cs
index 5ed6f774..7b36ef62 100644
--- a/src/OpenRiaServices.Client.DomainClients.Http/Framework/Http/BinaryHttpDomainClient.cs
+++ b/src/OpenRiaServices.Client.DomainClients.Http/Framework/Http/BinaryHttpDomainClient.cs
@@ -2,6 +2,7 @@
using System.IO;
using System.Net.Http;
using System.Runtime.Serialization;
+using System.Xml;
namespace OpenRiaServices.Client.DomainClients.Http
{
@@ -12,9 +13,15 @@ namespace OpenRiaServices.Client.DomainClients.Http
sealed class BinaryHttpDomainClient : DataContractHttpDomainClient
{
internal const string MediaType = "application/msbin1";
+ private readonly XmlDictionaryReaderQuotas _readerQuotas;
+ private readonly IXmlDictionary _dictionary;
- public BinaryHttpDomainClient(HttpClient httpClient, Type serviceInterface) : base(httpClient, serviceInterface)
+ public BinaryHttpDomainClient(HttpClient httpClient, Type serviceInterface, XmlDictionaryReaderQuotas readerQuotas, IXmlDictionary dictionary)
+ : base(httpClient, serviceInterface)
{
+ ArgumentNullException.ThrowIfNull(readerQuotas);
+ _readerQuotas = CreateQuotasCopy(readerQuotas);
+ _dictionary = dictionary;
}
private protected override string ContentType => MediaType;
@@ -22,12 +29,19 @@ public BinaryHttpDomainClient(HttpClient httpClient, Type serviceInterface) : ba
private protected override System.Xml.XmlDictionaryReader CreateReader(Stream stream)
{
- return System.Xml.XmlDictionaryReader.CreateBinaryReader(stream, System.Xml.XmlDictionaryReaderQuotas.Max);
+ return XmlDictionaryReader.CreateBinaryReader(stream, _dictionary, _readerQuotas);
}
private protected override System.Xml.XmlDictionaryWriter CreateWriter(Stream stream)
{
- return System.Xml.XmlDictionaryWriter.CreateBinaryWriter(stream, null, null, ownsStream: false);
+ return XmlDictionaryWriter.CreateBinaryWriter(stream, _dictionary, null, ownsStream: false);
+ }
+
+ private static XmlDictionaryReaderQuotas CreateQuotasCopy(XmlDictionaryReaderQuotas source)
+ {
+ var copy = new XmlDictionaryReaderQuotas();
+ source.CopyTo(copy);
+ return copy;
}
}
}
diff --git a/src/OpenRiaServices.Client.DomainClients.Http/Framework/Http/XmlHttpDomainClient.cs b/src/OpenRiaServices.Client.DomainClients.Http/Framework/Http/XmlHttpDomainClient.cs
index 29014c18..aa07d505 100644
--- a/src/OpenRiaServices.Client.DomainClients.Http/Framework/Http/XmlHttpDomainClient.cs
+++ b/src/OpenRiaServices.Client.DomainClients.Http/Framework/Http/XmlHttpDomainClient.cs
@@ -2,6 +2,7 @@
using System.IO;
using System.Net.Http;
using System.Runtime.Serialization;
+using System.Xml;
namespace OpenRiaServices.Client.DomainClients.Http
{
@@ -13,21 +14,32 @@ sealed class XmlHttpDomainClient : DataContractHttpDomainClient
internal const string MediaType = "application/xml";
private readonly System.Text.Encoding _encoding = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ private readonly XmlDictionaryReaderQuotas _readerQuotas;
- public XmlHttpDomainClient(HttpClient httpClient, Type serviceInterface) : base(httpClient, serviceInterface)
+ public XmlHttpDomainClient(HttpClient httpClient, Type serviceInterface, XmlDictionaryReaderQuotas readerQuotas)
+ : base(httpClient, serviceInterface)
{
+ ArgumentNullException.ThrowIfNull(readerQuotas);
+ _readerQuotas = CreateQuotasCopy(readerQuotas);
}
private protected override string ContentType => MediaType;
private protected override System.Xml.XmlDictionaryReader CreateReader(Stream stream)
{
- return System.Xml.XmlDictionaryReader.CreateTextReader(stream, System.Xml.XmlDictionaryReaderQuotas.Max);
+ return XmlDictionaryReader.CreateTextReader(stream, _readerQuotas);
}
private protected override System.Xml.XmlDictionaryWriter CreateWriter(Stream stream)
{
- return System.Xml.XmlDictionaryWriter.CreateTextWriter(stream, _encoding, ownsStream: false);
+ return XmlDictionaryWriter.CreateTextWriter(stream, _encoding, ownsStream: false);
+ }
+
+ private static XmlDictionaryReaderQuotas CreateQuotasCopy(XmlDictionaryReaderQuotas source)
+ {
+ var copy = new XmlDictionaryReaderQuotas();
+ source.CopyTo(copy);
+ return copy;
}
}
}
diff --git a/src/OpenRiaServices.Client.DomainClients.Http/Framework/XmlHttpDomainClientFactory.cs b/src/OpenRiaServices.Client.DomainClients.Http/Framework/XmlHttpDomainClientFactory.cs
index 3a0fc721..4a95e0c7 100644
--- a/src/OpenRiaServices.Client.DomainClients.Http/Framework/XmlHttpDomainClientFactory.cs
+++ b/src/OpenRiaServices.Client.DomainClients.Http/Framework/XmlHttpDomainClientFactory.cs
@@ -1,6 +1,6 @@
-using System;
-using System.Diagnostics;
+using System;
using System.Net.Http;
+using System.Xml;
using OpenRiaServices.Client.DomainClients.Http;
namespace OpenRiaServices.Client.DomainClients
@@ -11,6 +11,11 @@ namespace OpenRiaServices.Client.DomainClients
///
public class XmlHttpDomainClientFactory : HttpDomainClientFactory
{
+ ///
+ /// Gets or sets the quotas used by when reading responses.
+ ///
+ public XmlDictionaryReaderQuotas ReaderQuotas { get; set; } = CreateMaxQuotas();
+
///
public XmlHttpDomainClientFactory(Uri serverBaseUri, HttpMessageHandler messageHandler) : base(serverBaseUri, messageHandler)
{
@@ -26,7 +31,14 @@ protected override DomainClient CreateDomainClientCore(Type serviceContract, Uri
{
HttpClient httpClient = CreateHttpClient(serviceUri, XmlHttpDomainClient.MediaType);
- return new XmlHttpDomainClient(httpClient, serviceContract);
+ return new XmlHttpDomainClient(httpClient, serviceContract, ReaderQuotas);
+ }
+
+ private static XmlDictionaryReaderQuotas CreateMaxQuotas()
+ {
+ var quotas = new XmlDictionaryReaderQuotas();
+ XmlDictionaryReaderQuotas.Max.CopyTo(quotas);
+ return quotas;
}
}
}
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesOptions.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesOptions.cs
index 35afcfb5..5fc48ff7 100644
--- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesOptions.cs
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesOptions.cs
@@ -30,7 +30,7 @@ public sealed class OpenRiaServicesOptions
/// List of all registered wire formats on descending order of priority.
/// First one is the default used for responses (when client do not specify an matching format)
///
- internal ISerializationProvider[] SerializationProviders { get; set; } = [new BinaryXmlSerializationProvider()];
+ internal ISerializationProvider[] SerializationProviders { get; set; } = [];
///
/// Adds a serialization provider to the list of supported formats.
@@ -46,7 +46,7 @@ internal void AddSerializationProvider(ISerializationProvider provider, bool def
&& SerializationProviders.OfType().FirstOrDefault() is { } existingDcs)
{
// Share the DataContractCache between the two providers to avoid duplicate work
- dataContractSerializationProvider._perDomainServiceDataContractCache = existingDcs._perDomainServiceDataContractCache;
+ dataContractSerializationProvider.CopyDataContractCacheFrom(existingDcs);
}
if (defaultProvider)
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesOptionsBuilder.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesOptionsBuilder.cs
index 1a39efa9..d11e2e4e 100644
--- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesOptionsBuilder.cs
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesOptionsBuilder.cs
@@ -1,5 +1,7 @@
using System;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using OpenRiaServices.Hosting.AspNetCore.Serialization;
namespace OpenRiaServices.Hosting.AspNetCore
{
@@ -32,34 +34,56 @@ internal OpenRiaServicesOptionsBuilder(IServiceCollection services)
/// If the Xml provider will be the default for responses (when content type is not specified)
public OpenRiaServicesOptionsBuilder AddXmlSerialization(bool defaultProvider = false)
{
- return AddSerializationProvider(new Serialization.TextXmlSerializationProvider(), defaultProvider);
+ return AddXmlSerialization(configure: null, defaultProvider);
}
///
- /// Removes all registered s.
- /// Useful for removing default serialization formats (application/msbin1).
+ /// Enables text based XML wire format (application/xml) in addition to built in binary Xml (application/msbin1),
+ /// with options configurable via a callback.
///
- public OpenRiaServicesOptionsBuilder ClearSerializationProviders()
+ /// Request should specify mime-type application/xml using Content-Type or Accept HTTP-headers.
+ ///
+ /// An optional callback to configure .
+ /// If the Xml provider will be the default for responses (when content type is not specified)
+ public OpenRiaServicesOptionsBuilder AddXmlSerialization(Action? configure, bool defaultProvider = false)
{
- Services.Configure(options =>
- {
- options.ClearSerializationProviders();
- });
+ if (configure is not null)
+ Services.Configure(configure);
+
+ Services.AddOptions()
+ .Configure((OpenRiaServicesOptions options, IOptions serializationOptions) =>
+ {
+ options.AddSerializationProvider(new TextXmlSerializationProvider(serializationOptions.Value), defaultProvider);
+ });
return this;
}
- private OpenRiaServicesOptionsBuilder AddSerializationProvider(Serialization.ISerializationProvider serializationProvider, bool defaultProvider)
+ ///
+ /// Configures the default binary XML (application/msbin1) serialization provider.
+ ///
+ ///
+ /// Use this to restrict reader quotas and mitigate denial-of-service risks for binary requests.
+ ///
+ /// A callback to configure .
+ public OpenRiaServicesOptionsBuilder ConfigureBinarySerialization(Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(configure);
+
+ Services.Configure(configure);
+
+ return this;
+ }
+
+ ///
+ /// Removes all registered s.
+ /// Useful for removing default serialization formats (application/msbin1).
+ ///
+ public OpenRiaServicesOptionsBuilder ClearSerializationProviders()
{
- // When adding options it might make sense to resolve the provider using DI so allowing default options configuration
- //Services.AddSingleton();
- //Services.AddOptions().Configure((OpenRiaServicesOptions opts, Serialization.TextXmlSerializationProvider provider) => { });
- // OR
- // Services.Configure(callback)
- // Services.AddOptions().Configure((OpenRiaServicesOptions opts, IOptions options)
Services.Configure(options =>
{
- options.AddSerializationProvider(serializationProvider, defaultProvider);
+ options.ClearSerializationProviders();
});
return this;
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesServiceCollectionExtensions.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesServiceCollectionExtensions.cs
index 0b6bcf38..7ae425a7 100644
--- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesServiceCollectionExtensions.cs
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/OpenRiaServicesServiceCollectionExtensions.cs
@@ -2,6 +2,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using OpenRiaServices.Hosting.AspNetCore.Serialization;
using OpenRiaServices.Server;
namespace OpenRiaServices.Hosting.AspNetCore
@@ -21,6 +23,13 @@ public static OpenRiaServicesOptionsBuilder AddOpenRiaServices(this IServiceColl
services.AddSingleton();
services.AddSingleton(services);
+ services.AddOptions()
+ .Configure((OpenRiaServicesOptions options, IOptions binaryOptions) =>
+ {
+ // Default to using the Binary XML serializer if no serialization providers were added.
+ options.SerializationProviders = [new BinaryXmlSerializationProvider(binaryOptions.Value)];
+ });
+
return new OpenRiaServicesOptionsBuilder(services);
}
@@ -32,8 +41,11 @@ public static OpenRiaServicesOptionsBuilder AddOpenRiaServices(this IServiceColl
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
+
+ var builder = AddOpenRiaServices(services);
+ // Run user supplied configuration after the default configuration so that we have already added a default serialization provider
services.Configure(configure);
- return AddOpenRiaServices(services);
+ return builder;
}
///
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/BinaryMessageReader.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/BinaryMessageReader.cs
index 6ffacec0..957cb645 100644
--- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/BinaryMessageReader.cs
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/BinaryMessageReader.cs
@@ -24,7 +24,7 @@ private BinaryMessageReader()
_currentReader = _binaryReader = XmlDictionaryReader.CreateBinaryReader(Array.Empty(), XmlDictionaryReaderQuotas.Max);
}
- public static BinaryMessageReader Rent(ArraySegment bytes, bool isBinary)
+ public static BinaryMessageReader Rent(ArraySegment bytes, bool isBinary, XmlDictionaryReaderQuotas quotas, IXmlDictionary? dictionary = null)
{
var messageReader = s_threadInstance ?? new BinaryMessageReader();
@@ -34,15 +34,15 @@ public static BinaryMessageReader Rent(ArraySegment bytes, bool isBinary)
if (isBinary)
{
- ((IXmlBinaryReaderInitializer)messageReader._binaryReader).SetInput(bytes.Array!, bytes.Offset, bytes.Count, dictionary: null, XmlDictionaryReaderQuotas.Max, null, null);
+ ((IXmlBinaryReaderInitializer)messageReader._binaryReader).SetInput(bytes.Array!, bytes.Offset, bytes.Count, dictionary: dictionary, quotas, null, null);
messageReader._currentReader = messageReader._binaryReader;
}
else
{
if (messageReader._textReader is IXmlTextReaderInitializer textReader)
- textReader.SetInput(bytes.Array!, bytes.Offset, bytes.Count, encoding: null, XmlDictionaryReaderQuotas.Max, null);
+ textReader.SetInput(bytes.Array!, bytes.Offset, bytes.Count, encoding: null, quotas, null);
else
- messageReader._textReader = XmlDictionaryReader.CreateTextReader(bytes.Array!, bytes.Offset, bytes.Count, XmlDictionaryReaderQuotas.Max);
+ messageReader._textReader = XmlDictionaryReader.CreateTextReader(bytes.Array!, bytes.Offset, bytes.Count, quotas);
messageReader._currentReader = messageReader._textReader;
}
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/BinaryMessageWriter.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/BinaryMessageWriter.cs
index 9d56aa0d..a900bc2f 100644
--- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/BinaryMessageWriter.cs
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/BinaryMessageWriter.cs
@@ -40,7 +40,7 @@ private BinaryMessageWriter()
_textWriter = XmlDictionaryWriter.CreateTextWriter(_stream);
}
- public static BinaryMessageWriter Rent(bool isBinary)
+ public static BinaryMessageWriter Rent(bool isBinary, IXmlDictionary? dictionary = null)
{
var messageWriter = s_threadInstance ?? new BinaryMessageWriter();
@@ -50,7 +50,18 @@ public static BinaryMessageWriter Rent(bool isBinary)
// Allocate first buffer
messageWriter._stream.Reset(messageWriter.EstimateMessageSize());
- messageWriter._currentWriter = isBinary ? messageWriter._binaryWriter : messageWriter._textWriter;
+
+ if (isBinary)
+ {
+ // Reinitialize the binary writer to apply current dictionary settings and ensure clean state
+ ((IXmlBinaryWriterInitializer)messageWriter._binaryWriter).SetOutput(messageWriter._stream, dictionary, session: null, ownsStream: false);
+ messageWriter._currentWriter = messageWriter._binaryWriter;
+ }
+ else
+ {
+ messageWriter._currentWriter = messageWriter._textWriter;
+ }
+
return messageWriter;
}
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractRequestSerializer.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractRequestSerializer.cs
index 534fe7fb..b67dc98f 100644
--- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractRequestSerializer.cs
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractRequestSerializer.cs
@@ -12,8 +12,8 @@ namespace OpenRiaServices.Hosting.AspNetCore.Serialization
{
internal sealed class TextXmlDataContractRequestSerializer : DataContractRequestSerializer
{
- public TextXmlDataContractRequestSerializer(DomainOperationEntry operation, DataContractCache dataContractCache)
- : base(operation, dataContractCache, isBinary: false)
+ public TextXmlDataContractRequestSerializer(DomainOperationEntry operation, DataContractCache dataContractCache, XmlSerializationOptions options)
+ : base(operation, dataContractCache, isBinary: false, options)
{
}
@@ -26,8 +26,8 @@ public override bool CanWrite(ReadOnlySpan contentType)
internal sealed class BinaryXmlDataContractRequestSerializer : DataContractRequestSerializer
{
- public BinaryXmlDataContractRequestSerializer(DomainOperationEntry operation, DataContractCache dataContractCache)
- : base(operation, dataContractCache, isBinary: true)
+ public BinaryXmlDataContractRequestSerializer(DomainOperationEntry operation, DataContractCache dataContractCache, BinarySerializationOptions options)
+ : base(operation, dataContractCache, isBinary: true, options)
{
}
@@ -46,6 +46,8 @@ internal abstract class DataContractRequestSerializer : RequestSerializer
private readonly DomainOperationEntry _operation;
private readonly string _responseName;
private readonly string _resultName;
+ private readonly XmlDictionaryReaderQuotas _readerQuotas;
+ private readonly IXmlDictionary? _dictionary;
private const string MessageRootElementName = "MessageRoot";
private const string QueryOptionsListElementName = "QueryOptions";
private const string QueryOptionElementName = "QueryOption";
@@ -57,7 +59,7 @@ internal abstract class DataContractRequestSerializer : RequestSerializer
private bool IsBinary { get; }
private string ContentType => IsBinary ? MimeTypes.BinaryXml : MimeTypes.TextXml;
- protected DataContractRequestSerializer(DomainOperationEntry operation, DataContractCache dataContractCache, bool isBinary)
+ protected DataContractRequestSerializer(DomainOperationEntry operation, DataContractCache dataContractCache, bool isBinary, SerializationOptions options)
{
Type actualReturnType = operation.Operation switch
{
@@ -73,6 +75,8 @@ protected DataContractRequestSerializer(DomainOperationEntry operation, DataCont
this._responseName = operation.Name + "Response";
this._resultName = operation.Name + "Result";
this._responseSerializer = dataContractCache.GetSerializer(actualReturnType);
+ this._readerQuotas = options.ReaderQuotas;
+ this._dictionary = isBinary ? options.Dictionary : null;
if (operation.Operation == DomainOperation.Custom)
{
@@ -102,7 +106,7 @@ protected DataContractRequestSerializer(DomainOperationEntry operation, DataCont
try
{
- using var reader = BinaryMessageReader.Rent(memory, IsBinary);
+ using var reader = BinaryMessageReader.Rent(memory, IsBinary, _readerQuotas, _dictionary);
return ReadQueryParametersFromBody(reader.XmlDictionaryReader, operation);
}
catch (Exception ex) when (ex is not BadHttpRequestException && !ExceptionHandlingUtility.IsFatal(ex))
@@ -296,7 +300,7 @@ public override async Task WriteErrorAsync(HttpContext context, DomainServiceFau
if (ct.IsCancellationRequested)
return;
- var messageWriter = BinaryMessageWriter.Rent(IsBinary);
+ var messageWriter = BinaryMessageWriter.Rent(IsBinary, _dictionary);
try
{
WriteFault(fault, messageWriter.XmlWriter);
@@ -362,7 +366,7 @@ public override async Task WriteResponseAsync(HttpContext context, object? resul
if (ct.IsCancellationRequested)
return;
- var messageWriter = BinaryMessageWriter.Rent(IsBinary);
+ var messageWriter = BinaryMessageWriter.Rent(IsBinary, _dictionary);
try
{
WriteResponse(messageWriter.XmlWriter, result);
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractSerializationProvider.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractSerializationProvider.cs
index d2986a9e..b47c0a55 100644
--- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractSerializationProvider.cs
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractSerializationProvider.cs
@@ -8,6 +8,13 @@ namespace OpenRiaServices.Hosting.AspNetCore.Serialization
{
internal sealed class BinaryXmlSerializationProvider : DataContractSerializationProvider
{
+ private readonly BinarySerializationOptions _options;
+
+ public BinaryXmlSerializationProvider(BinarySerializationOptions? options = null)
+ {
+ _options = options ?? new BinarySerializationOptions();
+ }
+
///
/// Creates a Binary-XML DataContract based serializer for the specified domain operation.
///
@@ -15,11 +22,20 @@ internal sealed class BinaryXmlSerializationProvider : DataContractSerialization
/// The data contract cache containing type metadata used by the serializer.
/// A configured for the given operation and data contract cache.
protected override DataContractRequestSerializer CreateOperationRequestSerialiser(DomainOperationEntry operation, DataContractCache dataContractCache)
- => new BinaryXmlDataContractRequestSerializer(operation, dataContractCache);
+ => new BinaryXmlDataContractRequestSerializer(operation, dataContractCache, _options);
}
internal sealed class TextXmlSerializationProvider : DataContractSerializationProvider
{
+ private readonly XmlSerializationOptions _options;
+
+ public TextXmlSerializationProvider(XmlSerializationOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ _options = options;
+ }
+
///
/// Creates a (Text) XML DataContract based serializer for the specified domain operation.
///
@@ -27,7 +43,7 @@ internal sealed class TextXmlSerializationProvider : DataContractSerializationPr
/// The data contract metadata cache for the operation's domain service type.
/// A that serializes request payloads using text XML.
protected override DataContractRequestSerializer CreateOperationRequestSerialiser(DomainOperationEntry operation, DataContractCache dataContractCache)
- => new TextXmlDataContractRequestSerializer(operation, dataContractCache);
+ => new TextXmlDataContractRequestSerializer(operation, dataContractCache, _options);
}
internal abstract class DataContractSerializationProvider() : ISerializationProvider
@@ -55,6 +71,16 @@ public RequestSerializer GetRequestSerializer(DomainOperationEntry operation)
return _serializers.GetOrAdd(key, serializer);
}
+ ///
+ /// Shares the reference from with this instance,
+ /// so that multiple providers reuse the same per-domain-service metadata and avoid duplicate work.
+ ///
+ /// The provider whose data contract cache reference should be adopted by this instance.
+ internal void CopyDataContractCacheFrom(DataContractSerializationProvider source)
+ {
+ _perDomainServiceDataContractCache = source._perDomainServiceDataContractCache;
+ }
+
///
/// Override in derived classes to create a DataContractRequestSerializer for the specified domain operation using the provided data contract cache.
///
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractSerializerOptions.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractSerializerOptions.cs
new file mode 100644
index 00000000..2ae9a08a
--- /dev/null
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractSerializerOptions.cs
@@ -0,0 +1,62 @@
+using System.Xml;
+
+namespace OpenRiaServices.Hosting.AspNetCore
+{
+ ///
+ /// Base options class for DataContract-based serializers used by OpenRIA Services.
+ ///
+ public class SerializationOptions
+ {
+ ///
+ /// Gets or sets the quotas applied to instances during deserialization.
+ ///
+ ///
+ /// Defaults to a mutable instance equivalent to to preserve existing behavior.
+ /// Restrict these quotas to limit resource consumption and mitigate denial-of-service attacks.
+ ///
+ public XmlDictionaryReaderQuotas ReaderQuotas { get; set; } = CreateMaxQuotas();
+
+ ///
+ /// Gets or sets the XML dictionary used for binary format compression.
+ /// Exposed publicly on only.
+ ///
+ internal IXmlDictionary? Dictionary { get; set; }
+
+ ///
+ /// Creates a new mutable with all quotas set to their maximum values.
+ ///
+ private static XmlDictionaryReaderQuotas CreateMaxQuotas()
+ {
+ var quotas = new XmlDictionaryReaderQuotas();
+ XmlDictionaryReaderQuotas.Max.CopyTo(quotas);
+ return quotas;
+ }
+ }
+
+ ///
+ /// Options for the binary XML (application/msbin1) DataContract serializer.
+ ///
+ public sealed class BinarySerializationOptions : SerializationOptions
+ {
+ ///
+ /// Gets or sets the XML dictionary used for binary format compression when reading and writing.
+ ///
+ ///
+ /// When set, the same dictionary is applied to both the
+ /// and the to enable consistent compression of element
+ /// and attribute names. Client and server must share the same dictionary.
+ ///
+ public new IXmlDictionary? Dictionary
+ {
+ get => base.Dictionary;
+ set => base.Dictionary = value;
+ }
+ }
+
+ ///
+ /// Options for the text XML (application/xml) DataContract serializer.
+ ///
+ public sealed class XmlSerializationOptions : SerializationOptions
+ {
+ }
+}
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/OpenRiaServices.Hosting.AspNetCore.csproj b/src/OpenRiaServices.Hosting.AspNetCore/Framework/OpenRiaServices.Hosting.AspNetCore.csproj
index ec0573ca..78179569 100644
--- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/OpenRiaServices.Hosting.AspNetCore.csproj
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/OpenRiaServices.Hosting.AspNetCore.csproj
@@ -6,7 +6,7 @@
OpenRiaServices.Hosting
$(NoWarn);CS1574;CS1573;CS1591;CS1572
- 1.4.0
+ 1.5.0
true
Daniel-Svensson
OpenRiaServices AspNetCore Hosting DomainServices AspNet WCF RIA Services Server
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/README.md b/src/OpenRiaServices.Hosting.AspNetCore/Framework/README.md
index 5f0a6ab6..8aa3f16f 100644
--- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/README.md
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/README.md
@@ -132,6 +132,42 @@ builder.Services.AddOpenRiaServices(o => { } )
.AddXmlSerialization();
```
+### Configuring serializer security quotas
+
+You can configure reader quotas to limit resource consumption and mitigate denial-of-service (DoS) attacks.
+By default all quotas are set to their maximum values to preserve backward compatibility.
+
+**Configure XML serialization quotas:**
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddOpenRiaServices()
+ .AddXmlSerialization(options =>
+ {
+ options.ReaderQuotas = new System.Xml.XmlDictionaryReaderQuotas
+ {
+ MaxStringContentLength = 1024 * 1024, // 1 MB
+ MaxArrayLength = 65536,
+ MaxDepth = 32,
+ };
+ });
+```
+
+**Configure binary XML serialization quotas (for the default binary provider):**
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddOpenRiaServices()
+ .ConfigureBinarySerialization(options =>
+ {
+ options.ReaderQuotas = new System.Xml.XmlDictionaryReaderQuotas
+ {
+ MaxStringContentLength = 1024 * 1024, // 1 MB
+ MaxArrayLength = 65536,
+ MaxDepth = 32,
+ };
+ });
+```
### Specifying endpoint routes
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Test/OpenRiaServices.Hosting.AspNetCore.Test/BadRequestTests.cs b/src/OpenRiaServices.Hosting.AspNetCore/Test/OpenRiaServices.Hosting.AspNetCore.Test/BadRequestTests.cs
index fec8f04a..bd34226c 100644
--- a/src/OpenRiaServices.Hosting.AspNetCore/Test/OpenRiaServices.Hosting.AspNetCore.Test/BadRequestTests.cs
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Test/OpenRiaServices.Hosting.AspNetCore.Test/BadRequestTests.cs
@@ -23,7 +23,7 @@ namespace OpenRiaServices.Hosting.AspNetCore
[TestClass]
public class BadRequestTests
{
- private static readonly OpenRiaServicesOptions s_options = new();
+ private static readonly OpenRiaServicesOptions s_options = new() { SerializationProviders = [new Serialization.BinaryXmlSerializationProvider()] };
[TestMethod]
[Description("Invoke operation: Missing parameter when calling InvokeOperationInvoker should throw BadHttpRequestException when invoked directly")]
@@ -54,7 +54,7 @@ public async Task TestInvoke_MissingParameter_InvokeThrows()
context.Request.QueryString = QueryString.FromUriComponent("?value=");
await Assert.ThrowsExactlyAsync(async () => await operationInvoker.Invoke(context));
-
+
// Wrong format
context.Request.QueryString = QueryString.FromUriComponent("?value=two");
var invalidFormatEx = await Assert.ThrowsExactlyAsync(async () => await operationInvoker.Invoke(context));
diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Test/OpenRiaServices.Hosting.AspNetCore.Test/Serialization/SerializationOptionsTests.cs b/src/OpenRiaServices.Hosting.AspNetCore/Test/OpenRiaServices.Hosting.AspNetCore.Test/Serialization/SerializationOptionsTests.cs
new file mode 100644
index 00000000..a5eeed90
--- /dev/null
+++ b/src/OpenRiaServices.Hosting.AspNetCore/Test/OpenRiaServices.Hosting.AspNetCore.Test/Serialization/SerializationOptionsTests.cs
@@ -0,0 +1,213 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OpenRiaServices.Server;
+
+namespace OpenRiaServices.Hosting.AspNetCore;
+
+[TestClass]
+public class SerializationOptionsTests
+{
+ // -------------------------------------------------------------------------
+ // Builder API tests
+ // -------------------------------------------------------------------------
+
+ [TestMethod]
+ [Description("ConfigureBinarySerialization replaces existing BinaryXmlSerializationProvider with configured one")]
+ public void Builder_ConfigureBinarySerialization_ReplacesDefaultBinaryProvider()
+ {
+ bool configureWasCalled = false;
+ var services = new ServiceCollection();
+ var builder = services.AddOpenRiaServices();
+ builder.ConfigureBinarySerialization(opts =>
+ {
+ configureWasCalled = true;
+ opts.ReaderQuotas = new XmlDictionaryReaderQuotas { MaxStringContentLength = 2048 };
+ });
+
+ var sp = services.BuildServiceProvider();
+ var options = sp.GetRequiredService>().Value;
+
+ Assert.IsTrue(configureWasCalled, "Configure callback should have been called");
+
+ int binaryCount = options.SerializationProviders.Count(p => p is BinaryXmlSerializationProvider);
+ Assert.AreEqual(1, binaryCount, "Should still have exactly one BinaryXmlSerializationProvider");
+ }
+
+ [TestMethod]
+ [Description("ConfigureBinarySerialization can be fluently chained")]
+ public void Builder_ConfigureBinarySerialization_ReturnsSameBuilder()
+ {
+ var services = new ServiceCollection();
+ var builder = services.AddOpenRiaServices();
+ var returned = builder.ConfigureBinarySerialization(_ => { });
+ Assert.AreSame(builder, returned, "ConfigureBinarySerialization should return the builder for chaining");
+ }
+
+ [TestMethod]
+ [Description("AddXmlSerialization with configure returns the builder for chaining")]
+ public void Builder_AddXmlSerialization_WithConfigure_ReturnsSameBuilder()
+ {
+ var services = new ServiceCollection();
+ var builder = services.AddOpenRiaServices();
+ var returned = builder.AddXmlSerialization(_ => { });
+ Assert.AreSame(builder, returned, "AddXmlSerialization should return the builder for chaining");
+ }
+
+ // -------------------------------------------------------------------------
+ // Reader quota enforcement tests (binary)
+ // -------------------------------------------------------------------------
+
+ [TestMethod]
+ [Description("Binary reader quota: MaxStringContentLength is enforced")]
+ public async Task BinaryReaderQuota_MaxStringContentLength_IsEnforced()
+ {
+ using var host = await CreateHost(binaryConfigure: opts =>
+ {
+ opts.ReaderQuotas = new XmlDictionaryReaderQuotas { MaxStringContentLength = 10 };
+ });
+
+ var client = host.GetTestClient();
+
+ // A request with a string param longer than 10 chars should fail
+ var longString = new string('a', 100);
+ using var tooLongContent = BuildBinaryInvokeRequest("EchoString", "value", longString);
+ await AssertBadRequestAsync(client.PostAsync("SerializationTestDomainService/EchoString", tooLongContent), "The maximum string content length quota (10) has been exceeded");
+
+ using var shortContent = BuildBinaryInvokeRequest("EchoString", "value", "hello");
+ var response = await client.PostAsync("SerializationTestDomainService/EchoString", shortContent);
+ Assert.AreEqual(System.Net.HttpStatusCode.OK, response.StatusCode,
+ "Request within quota should succeed");
+ }
+
+ // -------------------------------------------------------------------------
+ // Reader quota enforcement tests (XML)
+ // -------------------------------------------------------------------------
+
+ [TestMethod]
+ [Description("XML reader quota: MaxStringContentLength is enforced")]
+ public async Task XmlReaderQuota_MaxStringContentLength_IsEnforced()
+ {
+ using var host = await CreateHost(xmlConfigure: opts =>
+ {
+ opts.ReaderQuotas = new XmlDictionaryReaderQuotas { MaxStringContentLength = 10 };
+ });
+
+ var client = host.GetTestClient();
+ var longString = new string('a', 100);
+ using var content = BuildXmlInvokeRequest("EchoString", "value", longString);
+
+ await AssertBadRequestAsync(client.PostAsync("SerializationTestDomainService/EchoString", content), "The maximum string content length quota (10) has been exceeded");
+ }
+
+ // -------------------------------------------------------------------------
+ // Helpers
+ // -------------------------------------------------------------------------
+
+ private static async Task CreateHost(
+ Action binaryConfigure = null,
+ Action xmlConfigure = null)
+ {
+ return await new HostBuilder()
+ .ConfigureWebHost(webBuilder =>
+ {
+ webBuilder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddRouting();
+ var oria = services.AddOpenRiaServices();
+ if (binaryConfigure is not null)
+ oria.ConfigureBinarySerialization(binaryConfigure);
+ if (xmlConfigure is not null)
+ oria.AddXmlSerialization(xmlConfigure);
+ services.AddDomainService();
+ })
+ .Configure(app =>
+ {
+ app.UseRouting();
+ app.UseEndpoints(e =>
+ {
+ e.MapOpenRiaServices(b => b.AddRegisteredDomainServices());
+ });
+ });
+ })
+ .StartAsync();
+ }
+
+ /// Asserts that a request results in HTTP 400 Bad Request (handles TestServer propagating the BadHttpRequestException).
+ private static async Task AssertBadRequestAsync(Task responseTask, string messageContents = null)
+ {
+ try
+ {
+ var response = await responseTask;
+ Assert.AreEqual(System.Net.HttpStatusCode.BadRequest, response.StatusCode,
+ "Expected HTTP 400 Bad Request");
+ }
+ catch (BadHttpRequestException ex)
+ {
+ // TestServer propagates BadHttpRequestException directly instead of converting it to HTTP 400
+ Assert.AreEqual(StatusCodes.Status400BadRequest, ex.StatusCode,
+ "Expected BadHttpRequestException with status 400");
+
+ if (messageContents is not null)
+ Assert.Contains(messageContents, ex.Message);
+ }
+ }
+
+ /// Builds a binary-XML POST body for an invoke operation with one string parameter.
+ private static ByteArrayContent BuildBinaryInvokeRequest(string operationName, string paramName, string paramValue)
+ {
+ using var ms = new MemoryStream();
+ using var writer = XmlDictionaryWriter.CreateBinaryWriter(ms, dictionary: null, session: null, ownsStream: false);
+ writer.WriteStartElement(operationName);
+ writer.WriteStartElement(paramName);
+ writer.WriteString(paramValue);
+ writer.WriteEndElement();
+ writer.WriteEndElement();
+ writer.Flush();
+ var content = new ByteArrayContent(ms.ToArray());
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/msbin1");
+ return content;
+ }
+
+ /// Builds a plain-XML POST body for an invoke operation with one string parameter.
+ private static ByteArrayContent BuildXmlInvokeRequest(string operationName, string paramName, string paramValue)
+ {
+ using var ms = new MemoryStream();
+ var settings = new XmlWriterSettings { Encoding = new UTF8Encoding(false), CloseOutput = false };
+ using var writer = XmlWriter.Create(ms, settings);
+ writer.WriteStartElement(operationName);
+ writer.WriteStartElement(paramName);
+ writer.WriteString(paramValue);
+ writer.WriteEndElement();
+ writer.WriteEndElement();
+ writer.Flush();
+ var content = new ByteArrayContent(ms.ToArray());
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/xml");
+ return content;
+ }
+
+ // -------------------------------------------------------------------------
+ // Test domain service
+ // -------------------------------------------------------------------------
+
+ [EnableClientAccess]
+ public class SerializationTestDomainService : DomainService
+ {
+ [Invoke(HasSideEffects = true)]
+ public string EchoString(string value) => value;
+ }
+}