Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,5 @@ $RECYCLE.BIN/
.DS_Store
*.nupkg
*.binlog
src/snk
src/Test/Desktop/OpenRiaServices.Common.DomainServices.Test/DataModels/ScenarioModels/northwind.map
10 changes: 10 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System;
using System.Net.Http;
using System.Xml;
using OpenRiaServices.Client.DomainClients.Http;

namespace OpenRiaServices.Client.DomainClients
Expand All @@ -11,6 +12,16 @@
public class BinaryHttpDomainClientFactory
: HttpDomainClientFactory
{
/// <summary>
/// Gets or sets the quotas used by <see cref="XmlDictionaryReader"/> when reading responses.
/// </summary>
public XmlDictionaryReaderQuotas ReaderQuotas { get; set; } = CreateMaxQuotas();

/// <summary>
/// Gets or sets the shared dictionary used for binary XML reader/writer compression.
/// </summary>
public IXmlDictionary? Dictionary { get; set; }

Check warning on line 23 in src/OpenRiaServices.Client.DomainClients.Http/Framework/BinaryHttpDomainClientFactory.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

See more on https://sonarcloud.io/project/issues?id=OpenRIAServices_OpenRiaServices&issues=AZ5KOIq7DK7bdrnOqsoF&open=AZ5KOIq7DK7bdrnOqsoF&pullRequest=578

/// <inheritdoc />
public BinaryHttpDomainClientFactory(Uri serverBaseUri, HttpMessageHandler messageHandler)
: base(serverBaseUri, messageHandler)
Expand All @@ -34,7 +45,14 @@
{
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Xml;

namespace OpenRiaServices.Client.DomainClients.Http
{
Expand All @@ -12,22 +13,35 @@
sealed class BinaryHttpDomainClient : DataContractHttpDomainClient
{
internal const string MediaType = "application/msbin1";
private readonly XmlDictionaryReaderQuotas _readerQuotas;
private readonly IXmlDictionary? _dictionary;

Check warning on line 17 in src/OpenRiaServices.Client.DomainClients.Http/Framework/Http/BinaryHttpDomainClient.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

See more on https://sonarcloud.io/project/issues?id=OpenRIAServices_OpenRiaServices&issues=AZ5KOIoPDK7bdrnOqsoD&open=AZ5KOIoPDK7bdrnOqsoD&pullRequest=578

public BinaryHttpDomainClient(HttpClient httpClient, Type serviceInterface) : base(httpClient, serviceInterface)
public BinaryHttpDomainClient(HttpClient httpClient, Type serviceInterface, XmlDictionaryReaderQuotas readerQuotas, IXmlDictionary? dictionary)

Check warning on line 19 in src/OpenRiaServices.Client.DomainClients.Http/Framework/Http/BinaryHttpDomainClient.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

See more on https://sonarcloud.io/project/issues?id=OpenRIAServices_OpenRiaServices&issues=AZ5KOIoPDK7bdrnOqsoE&open=AZ5KOIoPDK7bdrnOqsoE&pullRequest=578
: base(httpClient, serviceInterface)
{
ArgumentNullException.ThrowIfNull(readerQuotas);
_readerQuotas = CreateQuotasCopy(readerQuotas);
_dictionary = dictionary;
}

private protected override string ContentType => MediaType;


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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Xml;

namespace OpenRiaServices.Client.DomainClients.Http
{
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,6 +11,11 @@ namespace OpenRiaServices.Client.DomainClients
/// </summary>
public class XmlHttpDomainClientFactory : HttpDomainClientFactory
{
/// <summary>
/// Gets or sets the quotas used by <see cref="XmlDictionaryReader"/> when reading responses.
/// </summary>
public XmlDictionaryReaderQuotas ReaderQuotas { get; set; } = CreateMaxQuotas();

/// <inheritdoc />
public XmlHttpDomainClientFactory(Uri serverBaseUri, HttpMessageHandler messageHandler) : base(serverBaseUri, messageHandler)
{
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
/// </summary>
internal ISerializationProvider[] SerializationProviders { get; set; } = [new BinaryXmlSerializationProvider()];
internal ISerializationProvider[] SerializationProviders { get; set; } = [];

/// <summary>
/// Adds a serialization provider to the list of supported formats.
Expand All @@ -46,7 +46,7 @@ internal void AddSerializationProvider(ISerializationProvider provider, bool def
&& SerializationProviders.OfType<DataContractSerializationProvider>().FirstOrDefault() is { } existingDcs)
{
// Share the DataContractCache between the two providers to avoid duplicate work
dataContractSerializationProvider._perDomainServiceDataContractCache = existingDcs._perDomainServiceDataContractCache;
dataContractSerializationProvider.CopyDataContractCacheFrom(existingDcs);
}

if (defaultProvider)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace OpenRiaServices.Hosting.AspNetCore
{
Expand Down Expand Up @@ -32,34 +33,56 @@ internal OpenRiaServicesOptionsBuilder(IServiceCollection services)
/// <param name="defaultProvider">If <see langword="true"/> the Xml provider will be the default for responses (when content type is not specified)</param>
public OpenRiaServicesOptionsBuilder AddXmlSerialization(bool defaultProvider = false)
{
return AddSerializationProvider(new Serialization.TextXmlSerializationProvider(), defaultProvider);
return AddXmlSerialization(configure: null, defaultProvider);
}

/// <summary>
/// Removes all registered <see cref="Serialization.ISerializationProvider" />s.
/// <para>Useful for removing default serialization formats (application/msbin1).</para>
/// Enables text based XML wire format (application/xml) in addition to built in binary Xml (application/msbin1),
/// with options configurable via a callback.
/// </summary>
public OpenRiaServicesOptionsBuilder ClearSerializationProviders()
/// <remarks>Request should specify mime-type <c>application/xml</c> using <c>Content-Type</c> or <c>Accept</c> HTTP-headers.
/// </remarks>
/// <param name="configure">An optional callback to configure <see cref="Serialization.XmlDataContractSerializerOptions"/>.</param>
/// <param name="defaultProvider">If <see langword="true"/> the Xml provider will be the default for responses (when content type is not specified)</param>
public OpenRiaServicesOptionsBuilder AddXmlSerialization(Action<Serialization.XmlDataContractSerializerOptions>? configure, bool defaultProvider = false)
{
Services.Configure<OpenRiaServicesOptions>(options =>
{
options.ClearSerializationProviders();
});
if (configure is not null)
Services.Configure(configure);

Services.AddOptions<OpenRiaServicesOptions>()
.Configure((OpenRiaServicesOptions options, IOptions<Serialization.XmlDataContractSerializerOptions> serializationOptions) =>
{
options.AddSerializationProvider(new Serialization.TextXmlSerializationProvider(serializationOptions.Value), defaultProvider);
});

return this;
}

private OpenRiaServicesOptionsBuilder AddSerializationProvider(Serialization.ISerializationProvider serializationProvider, bool defaultProvider)
/// <summary>
/// Configures the default binary XML (<c>application/msbin1</c>) serialization provider.
/// </summary>
/// <remarks>
/// Use this to restrict reader quotas and mitigate denial-of-service risks for binary requests.
/// </remarks>
/// <param name="configure">A callback to configure <see cref="Serialization.BinaryDataContractSerializerOptions"/>.</param>
public OpenRiaServicesOptionsBuilder ConfigureBinarySerialization(Action<Serialization.BinaryDataContractSerializerOptions> configure)
{
ArgumentNullException.ThrowIfNull(configure);

Services.Configure<Serialization.BinaryDataContractSerializerOptions>(configure);

return this;
}

/// <summary>
/// Removes all registered <see cref="Serialization.ISerializationProvider" />s.
/// <para>Useful for removing default serialization formats (application/msbin1).</para>
/// </summary>
public OpenRiaServicesOptionsBuilder ClearSerializationProviders()
{
// When adding options it might make sense to resolve the provider using DI so allowing default options configuration
//Services.AddSingleton<Serialization.TextXmlSerializationProvider>();
//Services.AddOptions<OpenRiaServicesOptions>().Configure((OpenRiaServicesOptions opts, Serialization.TextXmlSerializationProvider provider) => { });
// OR
// Services.Configure<XmlSerializationOptions>(callback)
// Services.AddOptions<OpenRiaServicesOptions>().Configure((OpenRiaServicesOptions opts, IOptions<XmlSerializationOptions> options)
Services.Configure<OpenRiaServicesOptions>(options =>
{
options.AddSerializationProvider(serializationProvider, defaultProvider);
options.ClearSerializationProviders();
});

return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,6 +23,13 @@ public static OpenRiaServicesOptionsBuilder AddOpenRiaServices(this IServiceColl
services.AddSingleton<OpenRiaServicesEndpointDataSource>();
services.AddSingleton(services);

services.AddOptions<OpenRiaServicesOptions>()
.Configure((OpenRiaServicesOptions options, IOptions<BinaryDataContractSerializerOptions> binaryOptions) =>
{
// Default to using the Binary XML serializer if no serialization providers were added.
options.SerializationProviders = [new BinaryXmlSerializationProvider(binaryOptions.Value)];
});

return new OpenRiaServicesOptionsBuilder(services);
}

Expand All @@ -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<OpenRiaServicesOptions>(configure);
return AddOpenRiaServices(services);
return builder;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private BinaryMessageReader()
_currentReader = _binaryReader = XmlDictionaryReader.CreateBinaryReader(Array.Empty<byte>(), XmlDictionaryReaderQuotas.Max);
}

public static BinaryMessageReader Rent(ArraySegment<byte> bytes, bool isBinary)
public static BinaryMessageReader Rent(ArraySegment<byte> bytes, bool isBinary, XmlDictionaryReaderQuotas quotas, IXmlDictionary? dictionary = null)
{
var messageReader = s_threadInstance ?? new BinaryMessageReader();

Expand All @@ -34,15 +34,15 @@ public static BinaryMessageReader Rent(ArraySegment<byte> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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;
}

Expand Down
Loading