mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 14:43:08 +08:00
TcpMessageParsers: bound ReadReverseHelloMessage strings to MaxEndpointUrlLength (audit F1)
The public-API XML doc on TcpMessageParsers promises "every decoder is deliberately allocation-bounded (limits drawn from TcpMessageLimits)". ReadReverseHelloMessage was the only parser in the file that did not honour that contract: both `decoder.ReadString(null)` calls fell back to the decoder context's `MaxStringLength`, which is `ushort.MaxValue = 65535` (DefaultEncodingLimits.MaxStringLength). That is 16x larger than `TcpMessageLimits.MaxEndpointUrlLength = 4096`, which is the customary stack ceiling for ApplicationUri / EndpointUrl-class fields and which ReadHelloMessage already passes explicitly. Per call, a malformed RHE chunk could allocate up to ~128 KiB of string data instead of the ~8 KiB the contract documents -- a 16x allocation amplification on the public-API surface for any consumer that trusts the documented bound (offline tooling, pcap decoders, CTT-style harnesses). Fix: pass `TcpMessageLimits.MaxEndpointUrlLength` to both ReadString calls in ReadReverseHelloMessage. Behaviour for valid inputs unchanged (typical ServerUri / EndpointUrl is well under 4 KiB). The XML `<exception>` clause updated to include BadEncodingLimitsExceeded so the contract matches the implementation. Added regression tests in Tests/Opc.Ua.Core.Tests/Stack/Transport/TcpMessageParsersTests.cs: - ReadReverseHelloMessageOversizedServerUriThrowsBadEncodingLimitsExceeded - ReadReverseHelloMessageOversizedEndpointUrlThrowsBadEncodingLimitsExceeded - ReadReverseHelloMessageValidStringsAreAccepted (positive path) Verified (net10.0): - Opc.Ua.Core.Tests TcpMessageParsersTests: 3/3 passed, 0 failed. - Opc.Ua.Network.Fuzz.Tests baseline preserved: 330/330 passed, 0 failed. Audit ref: F1 in plans/security-audit (medium severity, fix-before-merge). Out of scope: the in-channel TcpReverseConnectChannel.ProcessReverseHelloMessage has the same unbounded read at lines 137-138. That is behaviour-parity with the public API before this fix; aligning the in-channel path with the new bound is a separate compat decision and is deferred to a follow-up PR.
This commit is contained in:
@@ -456,13 +456,16 @@ namespace Opc.Ua.Bindings
|
||||
/// </returns>
|
||||
/// <exception cref="ServiceResultException">
|
||||
/// The body is malformed
|
||||
/// (<see cref="StatusCodes.BadDecodingError"/>).
|
||||
/// (<see cref="StatusCodes.BadDecodingError"/>) or either of the
|
||||
/// length-prefixed string fields exceeds
|
||||
/// <see cref="TcpMessageLimits.MaxEndpointUrlLength"/>
|
||||
/// (<see cref="StatusCodes.BadEncodingLimitsExceeded"/>).
|
||||
/// </exception>
|
||||
public static ReverseHelloMessage ReadReverseHelloMessage(ArraySegment<byte> body)
|
||||
{
|
||||
using var decoder = CreateDecoder(body);
|
||||
string? serverUri = decoder.ReadString(null);
|
||||
string? endpointUrl = decoder.ReadString(null);
|
||||
string? serverUri = decoder.ReadString(null, TcpMessageLimits.MaxEndpointUrlLength);
|
||||
string? endpointUrl = decoder.ReadString(null, TcpMessageLimits.MaxEndpointUrlLength);
|
||||
|
||||
return new ReverseHelloMessage(serverUri, endpointUrl);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/* ========================================================================
|
||||
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
||||
*
|
||||
* OPC Foundation MIT License 1.00
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* The complete license agreement can be found here:
|
||||
* http://opcfoundation.org/License/MIT/1.00/
|
||||
* ======================================================================*/
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using Opc.Ua.Bindings;
|
||||
|
||||
namespace Opc.Ua.Core.Tests.Stack.Transport
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the stateless UA-TCP / UA-SC chunk parsers in
|
||||
/// <see cref="TcpMessageParsers"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("TcpMessageParsersTests")]
|
||||
[SetCulture("en-us")]
|
||||
[SetUICulture("en-us")]
|
||||
[Parallelizable]
|
||||
public class TcpMessageParsersTests
|
||||
{
|
||||
[Test]
|
||||
public void ReadReverseHelloMessageOversizedServerUriThrowsBadEncodingLimitsExceeded()
|
||||
{
|
||||
byte[] body = BuildReverseHelloBody(
|
||||
serverUri: new string('A', TcpMessageLimits.MaxEndpointUrlLength + 1),
|
||||
endpointUrl: "opc.tcp://localhost:4840");
|
||||
|
||||
ServiceResultException ex = Assert.Throws<ServiceResultException>(
|
||||
() => TcpMessageParsers.ReadReverseHelloMessage(new ArraySegment<byte>(body)));
|
||||
|
||||
Assert.That(ex.StatusCode, Is.EqualTo((StatusCode)StatusCodes.BadEncodingLimitsExceeded));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReadReverseHelloMessageOversizedEndpointUrlThrowsBadEncodingLimitsExceeded()
|
||||
{
|
||||
byte[] body = BuildReverseHelloBody(
|
||||
serverUri: "urn:test:server",
|
||||
endpointUrl: new string('B', TcpMessageLimits.MaxEndpointUrlLength + 1));
|
||||
|
||||
ServiceResultException ex = Assert.Throws<ServiceResultException>(
|
||||
() => TcpMessageParsers.ReadReverseHelloMessage(new ArraySegment<byte>(body)));
|
||||
|
||||
Assert.That(ex.StatusCode, Is.EqualTo((StatusCode)StatusCodes.BadEncodingLimitsExceeded));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReadReverseHelloMessageValidStringsAreAccepted()
|
||||
{
|
||||
const string serverUri = "urn:reverse:server";
|
||||
const string endpointUrl = "opc.tcp://gateway.example.com:4840/UA/Server";
|
||||
|
||||
byte[] body = BuildReverseHelloBody(serverUri, endpointUrl);
|
||||
|
||||
ReverseHelloMessage decoded = TcpMessageParsers.ReadReverseHelloMessage(
|
||||
new ArraySegment<byte>(body));
|
||||
|
||||
Assert.That(decoded.ServerUri, Is.EqualTo(serverUri));
|
||||
Assert.That(decoded.EndpointUrl, Is.EqualTo(endpointUrl));
|
||||
}
|
||||
|
||||
private static byte[] BuildReverseHelloBody(string serverUri, string endpointUrl)
|
||||
{
|
||||
using var encoder = new BinaryEncoder(ServiceMessageContext.CreateEmpty(null));
|
||||
encoder.WriteString(null, serverUri);
|
||||
encoder.WriteString(null, endpointUrl);
|
||||
return encoder.CloseAndReturnBuffer();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user