From 6e5351ce756f7908d53ed512aacb25c85e58ef03 Mon Sep 17 00:00:00 2001 From: Jeff <37258233+YAJeff@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:25:24 -0700 Subject: [PATCH] Reject duplicate MQTT 5 PUBLISH singleton properties (#2261) --- .../MqttV5PublishPropertyValidation_Tests.cs | 40 +++++++++++++++++++ .../Formatter/V5/MqttV5PacketDecoder.cs | 20 ++++++++++ 2 files changed, 60 insertions(+) create mode 100644 Source/MQTTnet.Tests/Formatter/MqttV5PublishPropertyValidation_Tests.cs diff --git a/Source/MQTTnet.Tests/Formatter/MqttV5PublishPropertyValidation_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttV5PublishPropertyValidation_Tests.cs new file mode 100644 index 00000000..e856f75d --- /dev/null +++ b/Source/MQTTnet.Tests/Formatter/MqttV5PublishPropertyValidation_Tests.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MQTTnet.Exceptions; +using MQTTnet.Formatter; +using MQTTnet.Packets; + +namespace MQTTnet.Tests.Formatter; + +[TestClass] +public sealed class MqttV5PublishPropertyValidation_Tests +{ + [TestMethod] + [DataRow("0100")] + [DataRow("0200000000")] + [DataRow("030000")] + [DataRow("08000161")] + [DataRow("090000")] + [DataRow("230001")] + public void Publish_Rejects_Duplicate_Singleton_Property(string propertyHex) + { + var property = Convert.FromHexString(propertyHex); + byte[] packet = [0x30, (byte)(4 + 2 * property.Length), 0, 1, (byte)'a', (byte)(2 * property.Length), .. property, .. property]; + + Assert.ThrowsExactly(() => MqttPacketSerializationHelper.DecodePacket(packet, MqttProtocolVersion.V500)); + } + + [TestMethod] + public void Publish_Allows_Repeated_Subscription_Identifiers_And_User_Properties() + { + byte[] packet = [0x30, 18, 0, 1, (byte)'a', 14, 0x0B, 1, 0x26, 0, 0, 0, 0, 0x0B, 1, 0x26, 0, 0, 0, 0]; + var decoded = (MqttPublishPacket)MqttPacketSerializationHelper.DecodePacket(packet, MqttProtocolVersion.V500); + + CollectionAssert.AreEqual(new uint[] { 1, 1 }, decoded.SubscriptionIdentifiers); + Assert.HasCount(2, decoded.UserProperties); + } + + +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs b/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs index 51d09901..0e3d6001 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs @@ -476,8 +476,28 @@ public sealed class MqttV5PacketDecoder } var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + var seenSingletonProperties = 0; while (propertiesReader.MoveNext()) { + // MQTT 5.0 section 3.3.2.3 permits only User Properties and + // Subscription Identifiers to occur more than once in PUBLISH. + var propertyFlag = propertiesReader.CurrentPropertyId switch + { + MqttPropertyId.PayloadFormatIndicator => 1, + MqttPropertyId.MessageExpiryInterval => 2, + MqttPropertyId.ContentType => 4, + MqttPropertyId.ResponseTopic => 8, + MqttPropertyId.CorrelationData => 16, + MqttPropertyId.TopicAlias => 32, + _ => 0 + }; + + if ((seenSingletonProperties & propertyFlag) != 0) + { + throw new MqttProtocolViolationException($"Property '{propertiesReader.CurrentPropertyId}' must not occur more than once in a PUBLISH packet."); + } + + seenSingletonProperties |= propertyFlag; if (propertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator) { packet.PayloadFormatIndicator = propertiesReader.ReadPayloadFormatIndicator();