diff --git a/Libraries/Esiur/Data/Codec.cs b/Libraries/Esiur/Data/Codec.cs index 757edb5..84dd933 100644 --- a/Libraries/Esiur/Data/Codec.cs +++ b/Libraries/Esiur/Data/Codec.cs @@ -649,6 +649,7 @@ public static class Codec public static byte[] Compose(object valueOrSource, Warehouse warehouse, EpConnection connection) { var tdu = ComposeInternal(valueOrSource, warehouse, connection); + ParserGuard.EnsureRemotePacketSize(connection, (ulong)tdu.Composed.LongLength); return tdu.Composed; } diff --git a/Libraries/Esiur/Data/DataSerializer.cs b/Libraries/Esiur/Data/DataSerializer.cs index 7fbecb1..550edc2 100644 --- a/Libraries/Esiur/Data/DataSerializer.cs +++ b/Libraries/Esiur/Data/DataSerializer.cs @@ -363,6 +363,10 @@ public static class DataSerializer public static Tdu StringComposer(object value, Warehouse warehouse, EpConnection connection) { var b = Encoding.UTF8.GetBytes((string)value); + ParserGuard.EnsureRemoteAllocation( + connection, + ParserGuard.MultiplySaturated((ulong)b.LongLength, 2), + "string"); return new Tdu(TduIdentifier.String, b, (uint)b.Length, null, null); } @@ -370,6 +374,10 @@ public static class DataSerializer public static Tdu ResourceLinkComposer(object value, Warehouse warehouse, EpConnection connection) { var b = Encoding.UTF8.GetBytes((ResourceLink)value); + ParserGuard.EnsureRemoteAllocation( + connection, + ParserGuard.MultiplySaturated((ulong)b.LongLength, 2), + "resource link"); return new Tdu(TduIdentifier.ResourceLink, b, (uint)b.Length, null, null); } @@ -446,12 +454,14 @@ public static class DataSerializer public static Tdu RawDataComposerFromArray(object value, Warehouse warehouse, EpConnection connection) { var b = (byte[])value; + ParserGuard.EnsureRemoteAllocation(connection, (ulong)b.LongLength, "raw data"); return new Tdu(TduIdentifier.RawData, b, (uint)b.Length, null, null); } public static Tdu RawDataComposerFromList(dynamic value, Warehouse warehouse, EpConnection connection) { var b = value as List; + ParserGuard.EnsureRemoteAllocation(connection, (ulong)b.Count, "raw data"); return new Tdu(TduIdentifier.RawData, b.ToArray(), (uint)b.Count, null, null); } @@ -516,6 +526,9 @@ public static class DataSerializer if (value == null) return null; + if (value is ICollection collection) + ParserGuard.EnsureRemoteCollectionCount(connection, collection.Count, GetTypedArrayElementSize(tru)); + if (tru.Identifier == TruIdentifier.Int32) { composed = GroupInt32Codec.Encode((IList)value); @@ -748,12 +761,18 @@ public static class DataSerializer // Pre-size the buffer from the element count (when known) to avoid repeated // List reallocations as items are appended. 4 bytes/element is a rough hint. - var rt = new List(value is ICollection collection ? collection.Count * 4 : 16); + var knownCount = value is ICollection collection ? collection.Count : -1; + if (knownCount >= 0) + ParserGuard.EnsureRemoteCollectionCount(connection, knownCount, IntPtr.Size); + var rt = new List(knownCount >= 0 ? knownCount * 4 : 16); Tdu? previous = null; + var count = 0; foreach (var i in value) { + if (knownCount < 0) + ParserGuard.EnsureRemoteCollectionCount(connection, ++count, IntPtr.Size); var tdu = Codec.ComposeInternal(i, warehouse, connection); if (previous != null && tdu.MatchType(previous.Value)) { @@ -862,13 +881,24 @@ public static class DataSerializer var rt = new List(); var map = (IMap)value; + var serialized = map.Serialize(); + ParserGuard.EnsureRemoteCollectionCount(connection, serialized.Length, IntPtr.Size); - foreach (var el in map.Serialize()) + foreach (var el in serialized) rt.AddRange(Codec.Compose(el, warehouse, connection)); return new Tdu(TduIdentifier.Map, rt.ToArray(), (uint)rt.Count, null, null); } + static int GetTypedArrayElementSize(Tru tru) + => tru.Identifier switch + { + TruIdentifier.Int16 or TruIdentifier.UInt16 => 2, + TruIdentifier.Int32 or TruIdentifier.UInt32 => 4, + TruIdentifier.Int64 or TruIdentifier.UInt64 => 8, + _ => IntPtr.Size + }; + /// /// Composes an indexed CLR structure using the compatible Map<byte, object> wire shape. /// diff --git a/Libraries/Esiur/Data/ParserGuard.cs b/Libraries/Esiur/Data/ParserGuard.cs index 42b373e..218a57b 100644 --- a/Libraries/Esiur/Data/ParserGuard.cs +++ b/Libraries/Esiur/Data/ParserGuard.cs @@ -15,6 +15,16 @@ public sealed class ParserLimitException : Exception } } +/// +/// Raised before transmission when a composed value exceeds a budget advertised by the peer. +/// +public sealed class RemoteParserLimitException : Exception +{ + public RemoteParserLimitException(string message) : base(message) + { + } +} + internal static class ParserGuard { internal static Warehouse? GetWarehouse(EpConnection? connection) @@ -70,4 +80,64 @@ internal static class ParserGuard internal static ulong MultiplySaturated(ulong value, ulong multiplier) => value > ulong.MaxValue / multiplier ? ulong.MaxValue : value * multiplier; + + internal static void EnsureRemotePacketSize(EpConnection? connection, ulong size) + { + var limit = connection?.Session?.RemoteMaximumPacketSize ?? 0; + if (limit > 0 && size > limit) + throw new RemoteParserLimitException( + $"Composed packet payload of {size} bytes exceeds the peer's advertised {limit}-byte limit."); + } + + internal static void EnsureRemoteAllocation( + EpConnection? connection, + ulong size, + string kind) + { + var limit = connection?.Session?.RemoteMaximumAllocationSize ?? 0; + if (limit > 0 && size > limit) + throw new RemoteParserLimitException( + $"Composed {kind} would allocate {size} bytes at the peer, exceeding its advertised {limit}-byte limit."); + } + + internal static void EnsureRemoteCollectionCount( + EpConnection? connection, + int count, + int estimatedBytesPerItem = 0) + { + var limit = connection?.Session?.RemoteMaximumCollectionItems ?? 0; + if (limit > 0 && count > limit) + throw new RemoteParserLimitException( + $"Composed collection has {count} items, exceeding the peer's advertised {limit}-item limit."); + + if (estimatedBytesPerItem > 0) + EnsureRemoteAllocation( + connection, + MultiplySaturated((ulong)count, (ulong)estimatedBytesPerItem), + "collection"); + } + + internal static void EnsureRemoteTypeMetadataDepth(EpConnection? connection, Tru? tru) + { + var limit = connection?.Session?.RemoteMaximumTypeMetadataDepth ?? 0; + if (limit <= 0 || tru == null) + return; + + var depth = GetTypeMetadataDepth(tru); + if (depth > limit) + throw new RemoteParserLimitException( + $"Composed TRU type metadata depth of {depth} exceeds the peer's advertised limit of {limit}."); + } + + static int GetTypeMetadataDepth(Tru tru) + { + if (tru is not TruComposite composite || composite.SubTypes == null || composite.SubTypes.Length == 0) + return 1; + + var maximumChildDepth = 0; + foreach (var child in composite.SubTypes) + maximumChildDepth = Math.Max(maximumChildDepth, GetTypeMetadataDepth(child)); + + return 1 + maximumChildDepth; + } } diff --git a/Libraries/Esiur/Data/Tdu.cs b/Libraries/Esiur/Data/Tdu.cs index 5908526..32405dd 100644 --- a/Libraries/Esiur/Data/Tdu.cs +++ b/Libraries/Esiur/Data/Tdu.cs @@ -176,6 +176,7 @@ public struct Tdu if (metadata == null) throw new Exception("Metadata must be provided for types."); + ParserGuard.EnsureRemoteTypeMetadataDepth(connection, metadata); var metadataData = metadata.Compose(connection); diff --git a/Libraries/Esiur/Net/Packets/EpAuthPacketHeader.cs b/Libraries/Esiur/Net/Packets/EpAuthPacketHeader.cs index 8cb3d8e..91f9c87 100644 --- a/Libraries/Esiur/Net/Packets/EpAuthPacketHeader.cs +++ b/Libraries/Esiur/Net/Packets/EpAuthPacketHeader.cs @@ -23,6 +23,11 @@ namespace Esiur.Net.Packets AuthenticationProtocol, AuthenticationData, ErrorMessage, - CipherNonce + CipherNonce, + MaximumPacketSize, + MaximumAllocationSize, + MaximumCollectionItems, + MaximumTypeMetadataDepth, + MaximumEncryptedRecordSize } } diff --git a/Libraries/Esiur/Protocol/EpConnection.cs b/Libraries/Esiur/Protocol/EpConnection.cs index d344733..4eb6409 100644 --- a/Libraries/Esiur/Protocol/EpConnection.cs +++ b/Libraries/Esiur/Protocol/EpConnection.cs @@ -369,17 +369,25 @@ public partial class EpConnection : NetworkConnection, IStore var provider = _session.EncryptionProvider ?? throw new InvalidOperationException("Session encryption is active without a provider."); var maximumRecordSize = ParsingWarehouse.Configuration.Encryption.MaximumRecordSize; + var remoteMaximumRecordSize = _session?.RemoteMaximumEncryptedRecordSize ?? 0; if (maximumRecordSize > 0 && (ulong)plaintext.LongLength + provider.MaximumRecordOverhead > maximumRecordSize) throw new ParserLimitException( $"Encrypted record would exceed the {maximumRecordSize}-byte limit."); + if (remoteMaximumRecordSize > 0 + && (ulong)plaintext.LongLength + provider.MaximumRecordOverhead > remoteMaximumRecordSize) + throw new RemoteParserLimitException( + $"Encrypted record would exceed the peer's advertised {remoteMaximumRecordSize}-byte limit."); var protectedPayload = cipher.Encrypt(plaintext); if (maximumRecordSize > 0 && protectedPayload.Length > maximumRecordSize) throw new InvalidOperationException( $"Encryption provider `{provider.DefaultName}` exceeded its declared record overhead."); + if (remoteMaximumRecordSize > 0 && protectedPayload.Length > remoteMaximumRecordSize) + throw new RemoteParserLimitException( + $"Encrypted record of {protectedPayload.Length} bytes exceeds the peer's advertised {remoteMaximumRecordSize}-byte limit."); if (protectedPayload.Length > int.MaxValue - EncryptedRecordHeaderSize) throw new ParserLimitException("Encrypted record exceeds the runtime allocation limit."); @@ -454,6 +462,7 @@ public partial class EpConnection : NetworkConnection, IStore } } + PopulateLocalLimitHeaders(); var headers = _session.LocalHeaders.Copy(); // Anonymous sessions still exchange typed records. They therefore @@ -491,6 +500,16 @@ public partial class EpConnection : NetworkConnection, IStore | ((byte)_session.EncryptionMode & 0x3)), headers); } + void PopulateLocalLimitHeaders() + { + var configuration = ParsingWarehouse.Configuration; + _session.LocalHeaders.MaximumPacketSize = configuration.Parser.MaximumPacketSize; + _session.LocalHeaders.MaximumAllocationSize = configuration.Parser.MaximumAllocationSize; + _session.LocalHeaders.MaximumCollectionItems = configuration.Parser.MaximumCollectionItems; + _session.LocalHeaders.MaximumTypeMetadataDepth = configuration.Parser.MaximumTypeMetadataDepth; + _session.LocalHeaders.MaximumEncryptedRecordSize = configuration.Encryption.MaximumRecordSize; + } + void PrepareEncryptionOffer() { if (_session.AuthenticationMode == AuthenticationMode.None) @@ -1653,6 +1672,7 @@ public partial class EpConnection : NetworkConnection, IStore ? $"anonymous:{RemoteEndPoint?.Address}" : remoteHeaders.Domain; _session.AuthenticationMode = _authPacket.AuthMode; + PopulateLocalLimitHeaders(); var localHeaders = _session.LocalHeaders.Copy(); if (!NegotiateEncryptionAsResponder(localHeaders)) @@ -1787,6 +1807,16 @@ public partial class EpConnection : NetworkConnection, IStore if (_session.AuthenticationMode == AuthenticationMode.None && _authPacket.Method == EpAuthPacketMethod.SessionEstablished) { + var remoteHeaders = new SessionHeaders(); + if (_authPacket.Tdu != null) + { + remoteHeaders = Codec.ParseIndexedType( + _authPacket.Tdu.Value, + ParsingWarehouse); + remoteHeaders.AuthenticationData = null; + } + _session.RemoteHeaders = remoteHeaders; + _session.Authenticated = true; _session.LocalIdentity = null; _session.RemoteIdentity = null; diff --git a/Libraries/Esiur/Protocol/EpConnectionProtocol.cs b/Libraries/Esiur/Protocol/EpConnectionProtocol.cs index 4fd2240..d18f4a8 100644 --- a/Libraries/Esiur/Protocol/EpConnectionProtocol.cs +++ b/Libraries/Esiur/Protocol/EpConnectionProtocol.cs @@ -329,7 +329,18 @@ partial class EpConnection //callbackCounter++; // avoid thread racing _requests.Add(c, reply); - SendRequestPacket(action, c, args); + try + { + SendRequestPacket(action, c, args); + } + catch (RemoteParserLimitException ex) + { + _requests.Take(c); + reply.TriggerError(new AsyncException( + ErrorType.Management, + (ushort)ExceptionCode.ParserLimitExceeded, + ex.Message)); + } return reply; } @@ -372,7 +383,18 @@ partial class EpConnection () => SendRequest(EpPacketRequest.ResumeExecution, callbackId)); _requests.Add(callbackId, reply); - SendRequestPacket(action, callbackId, args); + try + { + SendRequestPacket(action, callbackId, args); + } + catch (RemoteParserLimitException ex) + { + _requests.Take(callbackId); + reply.TriggerError(new AsyncException( + ErrorType.Management, + (ushort)ExceptionCode.ParserLimitExceeded, + ex.Message)); + } return reply; } @@ -387,7 +409,18 @@ partial class EpConnection () => SendRequest(EpPacketRequest.ResumeExecution, callbackId)); _requests.Add(callbackId, reply); - SendRequestPacket(action, callbackId, args); + try + { + SendRequestPacket(action, callbackId, args); + } + catch (RemoteParserLimitException ex) + { + _requests.Take(callbackId); + reply.TriggerError(new AsyncException( + ErrorType.Management, + (ushort)ExceptionCode.ParserLimitExceeded, + ex.Message)); + } return reply; } @@ -502,28 +535,42 @@ partial class EpConnection if (Instance == null) return; - if (args.Length == 0) + try { - var bl = new BinaryList(); - bl.AddUInt8((byte)(0x80 | (byte)action)) - .AddUInt32(callbackId); - Send(bl.ToArray()); + if (args.Length == 0) + { + var bl = new BinaryList(); + bl.AddUInt8((byte)(0x80 | (byte)action)) + .AddUInt32(callbackId); + Send(bl.ToArray()); + } + else if (args.Length == 1) + { + var bl = new BinaryList(); + bl.AddUInt8((byte)(0xA0 | (byte)action)) + .AddUInt32(callbackId) + .AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this)); + Send(bl.ToArray()); + } + else + { + var bl = new BinaryList(); + bl.AddUInt8((byte)(0xA0 | (byte)action)) + .AddUInt32(callbackId) + .AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this)); + Send(bl.ToArray()); + } } - if (args.Length == 1) + catch (RemoteParserLimitException ex) when ( + action != EpPacketReply.PermissionError + && action != EpPacketReply.ExecutionError + && action != EpPacketReply.Warning) { - var bl = new BinaryList(); - bl.AddUInt8((byte)(0xA0 | (byte)action)) - .AddUInt32(callbackId) - .AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this)); - Send(bl.ToArray()); - } - else - { - var bl = new BinaryList(); - bl.AddUInt8((byte)(0xA0 | (byte)action)) - .AddUInt32(callbackId) - .AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this)); - Send(bl.ToArray()); + SendError( + ErrorType.Exception, + callbackId, + (ushort)ExceptionCode.ParserLimitExceeded, + ex.Message); } } @@ -677,7 +724,19 @@ partial class EpConnection return; } - var pr = Codec.Parse(tdu.Value, this, null); + object pr; + try + { + pr = Codec.Parse(tdu.Value, this, null); + } + catch (ParserLimitException ex) + { + req.TriggerError(new AsyncException( + ErrorType.Management, + (ushort)ExceptionCode.ParserLimitExceeded, + ex.Message)); + return; + } if (pr is AsyncReply asyncReply) { @@ -724,7 +783,20 @@ partial class EpConnection return; } - var value = Codec.Parse(tdu, this, null); + object value; + try + { + value = Codec.Parse(tdu, this, null); + } + catch (ParserLimitException ex) + { + _requests.Take(callbackId); + req.TriggerError(new AsyncException( + ErrorType.Management, + (ushort)ExceptionCode.ParserLimitExceeded, + ex.Message)); + return; + } if (value is AsyncReply reply) { @@ -819,7 +891,20 @@ partial class EpConnection if (req == null) return; - var value = Codec.Parse(tdu, this, null); + object value; + try + { + value = Codec.Parse(tdu, this, null); + } + catch (ParserLimitException ex) + { + _requests.Take(callbackId); + req.TriggerError(new AsyncException( + ErrorType.Management, + (ushort)ExceptionCode.ParserLimitExceeded, + ex.Message)); + return; + } if (value is AsyncReply asyncReply) { @@ -3993,6 +4078,14 @@ partial class EpConnection private void Instance_PropertyModified(PropertyModificationInfo info) { + // Attachment permission does not imply property-read permission. A + // method-only resource can be attached so its exported functions are + // callable while all of its properties remain private. Re-evaluate the + // property operation before broadcasting each modification, matching + // the permission checks already applied to event notifications. + if (!IsOperationAllowed(info.Resource, info.PropertyDef, ActionType.GetProperty)) + return; + SendNotification(EpPacketNotification.PropertyModified, info.Resource.Instance.Id, info.Cursor.Generation.ToByteArray(), diff --git a/Libraries/Esiur/Security/Authority/Session.cs b/Libraries/Esiur/Security/Authority/Session.cs index 405e1fb..1aa3a5f 100644 --- a/Libraries/Esiur/Security/Authority/Session.cs +++ b/Libraries/Esiur/Security/Authority/Session.cs @@ -100,6 +100,21 @@ public sealed class SessionHeaders : IndexedStructure [Index((int)EpAuthPacketHeader.CipherNonce)] public byte[]? CipherNonce { get; set; } + [Index((int)EpAuthPacketHeader.MaximumPacketSize)] + public uint? MaximumPacketSize { get; set; } + + [Index((int)EpAuthPacketHeader.MaximumAllocationSize)] + public uint? MaximumAllocationSize { get; set; } + + [Index((int)EpAuthPacketHeader.MaximumCollectionItems)] + public int? MaximumCollectionItems { get; set; } + + [Index((int)EpAuthPacketHeader.MaximumTypeMetadataDepth)] + public int? MaximumTypeMetadataDepth { get; set; } + + [Index((int)EpAuthPacketHeader.MaximumEncryptedRecordSize)] + public uint? MaximumEncryptedRecordSize { get; set; } + internal SessionHeaders Copy() => (SessionHeaders)MemberwiseClone(); } @@ -121,6 +136,12 @@ public class Session public SessionHeaders LocalHeaders { get; set; } = new SessionHeaders(); public SessionHeaders RemoteHeaders { get; set; } = new SessionHeaders(); + public uint RemoteMaximumPacketSize => RemoteHeaders?.MaximumPacketSize ?? 0; + public uint RemoteMaximumAllocationSize => RemoteHeaders?.MaximumAllocationSize ?? 0; + public int RemoteMaximumCollectionItems => RemoteHeaders?.MaximumCollectionItems ?? 0; + public int RemoteMaximumTypeMetadataDepth => RemoteHeaders?.MaximumTypeMetadataDepth ?? 0; + public uint RemoteMaximumEncryptedRecordSize => RemoteHeaders?.MaximumEncryptedRecordSize ?? 0; + //public AuthenticationMethod AuthenticationMethod { get; set; } //public AuthenticationMethod RemoteMethod { get; set; } diff --git a/Tests/Unit/Integration/AttachmentSecurityTests.cs b/Tests/Unit/Integration/AttachmentSecurityTests.cs index 47c7970..44f3991 100644 --- a/Tests/Unit/Integration/AttachmentSecurityTests.cs +++ b/Tests/Unit/Integration/AttachmentSecurityTests.cs @@ -1,5 +1,10 @@ using Esiur.Core; +using Esiur.Data; +using Esiur.Data.Types; using Esiur.Protocol; +using Esiur.Resource; +using Esiur.Security.Authority; +using Esiur.Security.Permissions; namespace Esiur.Tests.Unit.Integration; @@ -36,10 +41,68 @@ public class AttachmentSecurityTests Assert.Equal(1, cluster.Connection.ResourceAttachRequestCount); } + [Fact] + public async Task PropertyNotifications_RespectPerPropertyReadPermission() + { + PropertyBroadcastResource? source = null; + await using var cluster = await IntegrationCluster.StartAsync(async warehouse => + { + var permissions = new PropertyBroadcastPermissions(); + warehouse.RegisterManager(permissions); + source = await warehouse.Put("sys/property-broadcast", new PropertyBroadcastResource()); + source.Instance!.Managers.Add(permissions); + }).WaitAsync(TimeSpan.FromSeconds(10)); + + var remote = (EpResource)await cluster.Connection.Get("sys/property-broadcast"); + var received = new List(); + remote.Instance.PropertyModified += info => received.Add(info.PropertyDef.Name); + + source!.Secret = 7; + source.Visible = 9; + + var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(3); + while (!received.Contains(nameof(PropertyBroadcastResource.Visible)) && + DateTime.UtcNow < deadline) + await Task.Delay(20); + + Assert.Contains(nameof(PropertyBroadcastResource.Visible), received); + Assert.DoesNotContain(nameof(PropertyBroadcastResource.Secret), received); + } + static Task StartCluster() => IntegrationCluster.StartAsync(async warehouse => { await warehouse.Put("sys/first", new RateLimitedResource()); await warehouse.Put("sys/second", new RateLimitedResource()); }); + + sealed class PropertyBroadcastPermissions : IPermissionsManager + { + public Map Settings { get; } = new(); + + public Ruling Applicable( + IResource resource, + Session session, + ActionType action, + MemberDef member, + object inquirer = null!) + { + if (action == ActionType.Attach) + return Ruling.Allowed; + if (action == ActionType.GetProperty) + return member?.Name == nameof(PropertyBroadcastResource.Secret) + ? Ruling.Denied + : Ruling.Allowed; + return Ruling.DontCare; + } + + public bool Initialize(Map settings, IResource resource) => true; + } +} + +[Resource] +public partial class PropertyBroadcastResource +{ + [Export] int secret; + [Export] int visible; } diff --git a/Tests/Unit/Integration/SessionHeadersIntegrationTests.cs b/Tests/Unit/Integration/SessionHeadersIntegrationTests.cs index 54b7ccb..e2a35ce 100644 --- a/Tests/Unit/Integration/SessionHeadersIntegrationTests.cs +++ b/Tests/Unit/Integration/SessionHeadersIntegrationTests.cs @@ -12,6 +12,46 @@ using System.Net; [Collection("Integration")] public class SessionHeadersIntegrationTests { + [Fact] + public async Task Handshake_ExchangesParserAndEncryptedRecordBudgetsInBothDirections() + { + await using var cluster = await IntegrationCluster + .StartAsync( + warehouse => + { + warehouse.Configuration.Parser.MaximumPacketSize = 7_100_001; + warehouse.Configuration.Parser.MaximumAllocationSize = 3_100_002; + warehouse.Configuration.Parser.MaximumCollectionItems = 51_003; + warehouse.Configuration.Parser.MaximumTypeMetadataDepth = 54; + warehouse.Configuration.Encryption.MaximumRecordSize = 7_101_004; + return Task.CompletedTask; + }, + populateClient: warehouse => + { + warehouse.Configuration.Parser.MaximumPacketSize = 6_200_001; + warehouse.Configuration.Parser.MaximumAllocationSize = 2_200_002; + warehouse.Configuration.Parser.MaximumCollectionItems = 42_003; + warehouse.Configuration.Parser.MaximumTypeMetadataDepth = 45; + warehouse.Configuration.Encryption.MaximumRecordSize = 6_201_004; + return Task.CompletedTask; + }) + .WaitAsync(TimeSpan.FromSeconds(10)); + + var serverConnection = Assert.Single(cluster.Server.Connections); + + Assert.Equal(7_100_001u, cluster.Connection.Session.RemoteMaximumPacketSize); + Assert.Equal(3_100_002u, cluster.Connection.Session.RemoteMaximumAllocationSize); + Assert.Equal(51_003, cluster.Connection.Session.RemoteMaximumCollectionItems); + Assert.Equal(54, cluster.Connection.Session.RemoteMaximumTypeMetadataDepth); + Assert.Equal(7_101_004u, cluster.Connection.Session.RemoteMaximumEncryptedRecordSize); + + Assert.Equal(6_200_001u, serverConnection.Session.RemoteMaximumPacketSize); + Assert.Equal(2_200_002u, serverConnection.Session.RemoteMaximumAllocationSize); + Assert.Equal(42_003, serverConnection.Session.RemoteMaximumCollectionItems); + Assert.Equal(45, serverConnection.Session.RemoteMaximumTypeMetadataDepth); + Assert.Equal(6_201_004u, serverConnection.Session.RemoteMaximumEncryptedRecordSize); + } + [Fact] public async Task AcceptedConnection_RaisesReadyAndDisconnectedLifecycleEvents() { diff --git a/Tests/Unit/ParserSecurityTests.cs b/Tests/Unit/ParserSecurityTests.cs index 5d452d0..85d25e9 100644 --- a/Tests/Unit/ParserSecurityTests.cs +++ b/Tests/Unit/ParserSecurityTests.cs @@ -7,6 +7,27 @@ namespace Esiur.Tests.Unit; public class ParserSecurityTests { + [Fact] + public void Composer_RejectsValuesAbovePeerAdvertisedBudgetsBeforeSend() + { + var warehouse = new Warehouse(); + var connection = new EpConnection(); + connection.Session.RemoteHeaders.MaximumPacketSize = 64; + connection.Session.RemoteHeaders.MaximumAllocationSize = 6; + connection.Session.RemoteHeaders.MaximumCollectionItems = 2; + + Assert.Throws(() => + Codec.Compose("four", warehouse, connection)); + Assert.Throws(() => + Codec.Compose(new object[] { 1, 2, 3 }, warehouse, connection)); + + connection.Session.RemoteHeaders.MaximumAllocationSize = 0; + connection.Session.RemoteHeaders.MaximumCollectionItems = 0; + connection.Session.RemoteHeaders.MaximumPacketSize = 3; + Assert.Throws(() => + Codec.Compose(new byte[] { 1, 2, 3, 4 }, warehouse, connection)); + } + [Fact] public void PacketParser_RejectsOversizedDeclarationBeforePayloadArrives() {