Skip to content

Commit 010bca6

Browse files
author
JianBo He
authored
Merge pull request #659 from emqx/fix/publish-protocol-error-handling
Fix malformed PUBLISH parsing and handle protocol errors safely
2 parents 573c231 + 12ac4f9 commit 010bca6

8 files changed

Lines changed: 233 additions & 35 deletions
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import Foundation
2+
import XCTest
3+
@testable import CocoaMQTT
4+
5+
final class CocoaMQTTReaderProtocolErrorTests: XCTestCase {
6+
7+
private final class SocketSpy: CocoaMQTTSocketProtocol {
8+
var enableSSL: Bool = false
9+
private(set) var disconnectCount = 0
10+
11+
func setDelegate(_ theDelegate: CocoaMQTTSocketDelegate?, delegateQueue: DispatchQueue?) {}
12+
func connect(toHost host: String, onPort port: UInt16) throws {}
13+
func connect(toHost host: String, onPort port: UInt16, withTimeout timeout: TimeInterval) throws {}
14+
func disconnect() { disconnectCount += 1 }
15+
func readData(toLength length: UInt, withTimeout timeout: TimeInterval, tag: Int) {}
16+
func write(_ data: Data, withTimeout timeout: TimeInterval, tag: Int) {}
17+
}
18+
19+
private final class ReaderDelegateSpy: CocoaMQTTReaderDelegate {
20+
private(set) var publishCount = 0
21+
private(set) var disconnectCount = 0
22+
private(set) var authCount = 0
23+
24+
func didReceive(_ reader: CocoaMQTTReader, connack: FrameConnAck) {}
25+
func didReceive(_ reader: CocoaMQTTReader, publish: FramePublish) { publishCount += 1 }
26+
func didReceive(_ reader: CocoaMQTTReader, puback: FramePubAck) {}
27+
func didReceive(_ reader: CocoaMQTTReader, pubrec: FramePubRec) {}
28+
func didReceive(_ reader: CocoaMQTTReader, pubrel: FramePubRel) {}
29+
func didReceive(_ reader: CocoaMQTTReader, pubcomp: FramePubComp) {}
30+
func didReceive(_ reader: CocoaMQTTReader, suback: FrameSubAck) {}
31+
func didReceive(_ reader: CocoaMQTTReader, unsuback: FrameUnsubAck) {}
32+
func didReceive(_ reader: CocoaMQTTReader, pingresp: FramePingResp) {}
33+
func didReceive(_ reader: CocoaMQTTReader, disconnect: FrameDisconnect) { disconnectCount += 1 }
34+
func didReceive(_ reader: CocoaMQTTReader, auth: FrameAuth) { authCount += 1 }
35+
}
36+
37+
func testMalformedPublishDisconnectsSocket() {
38+
CocoaMQTTStorage()?.setMQTTVersion("3.1.1")
39+
40+
let socket = SocketSpy()
41+
let delegate = ReaderDelegateSpy()
42+
let reader = CocoaMQTTReader(socket: socket, delegate: delegate)
43+
44+
reader.headerReady(FrameType.publish.rawValue)
45+
reader.lengthReady(0x06)
46+
reader.payloadReady(Data([0x00, 0x00, 0x41, 0x41, 0x41, 0x41]))
47+
48+
XCTAssertEqual(socket.disconnectCount, 1)
49+
XCTAssertEqual(delegate.publishCount, 0)
50+
}
51+
52+
func testUnknownFrameTypeDisconnectsSocket() {
53+
let socket = SocketSpy()
54+
let delegate = ReaderDelegateSpy()
55+
let reader = CocoaMQTTReader(socket: socket, delegate: delegate)
56+
57+
reader.headerReady(0x00)
58+
reader.lengthReady(0x00)
59+
60+
XCTAssertEqual(socket.disconnectCount, 1)
61+
XCTAssertEqual(delegate.publishCount, 0)
62+
}
63+
64+
func testMQTT5DisconnectFrameDoesNotProtocolError() {
65+
CocoaMQTTStorage()?.setMQTTVersion("5.0")
66+
67+
let socket = SocketSpy()
68+
let delegate = ReaderDelegateSpy()
69+
let reader = CocoaMQTTReader(socket: socket, delegate: delegate)
70+
71+
reader.headerReady(FrameType.disconnect.rawValue)
72+
reader.lengthReady(0x00)
73+
74+
XCTAssertEqual(socket.disconnectCount, 0)
75+
XCTAssertEqual(delegate.disconnectCount, 1)
76+
}
77+
78+
func testMQTT5AuthFrameDoesNotProtocolError() {
79+
CocoaMQTTStorage()?.setMQTTVersion("5.0")
80+
81+
let socket = SocketSpy()
82+
let delegate = ReaderDelegateSpy()
83+
let reader = CocoaMQTTReader(socket: socket, delegate: delegate)
84+
85+
reader.headerReady(FrameType.auth.rawValue)
86+
reader.lengthReady(0x00)
87+
88+
XCTAssertEqual(socket.disconnectCount, 0)
89+
XCTAssertEqual(delegate.authCount, 1)
90+
}
91+
92+
func testMQTT311RejectsMQTT5OnlyDisconnectFrame() {
93+
CocoaMQTTStorage()?.setMQTTVersion("3.1.1")
94+
95+
let socket = SocketSpy()
96+
let delegate = ReaderDelegateSpy()
97+
let reader = CocoaMQTTReader(socket: socket, delegate: delegate)
98+
99+
reader.headerReady(FrameType.disconnect.rawValue)
100+
reader.lengthReady(0x00)
101+
102+
XCTAssertEqual(socket.disconnectCount, 1)
103+
XCTAssertEqual(delegate.disconnectCount, 0)
104+
}
105+
}

CocoaMQTTTests/FrameTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@ class FrameTests: XCTestCase {
142142
XCTAssertEqual(f1?.payload().count, 0)
143143
}
144144

145+
func testFramePublishRejectsZeroLengthTopic() {
146+
CocoaMQTTStorage()?.setMQTTVersion("3.1.1")
147+
let frame = FramePublish(packetFixedHeaderType: FrameType.publish.rawValue, bytes: [0x00, 0x00, 0x41, 0x41, 0x41, 0x41])
148+
XCTAssertNil(frame)
149+
}
150+
151+
func testFramePublishRejectsZeroLengthTopicInMQTT5WithoutAlias() {
152+
CocoaMQTTStorage()?.setMQTTVersion("5.0")
153+
defer { CocoaMQTTStorage()?.setMQTTVersion("3.1.1") }
154+
155+
let frame = FramePublish(packetFixedHeaderType: FrameType.publish.rawValue, bytes: [0x00, 0x00, 0x00])
156+
XCTAssertNil(frame)
157+
}
158+
159+
func testFramePublishAllowsZeroLengthTopicInMQTT5WithAlias() {
160+
CocoaMQTTStorage()?.setMQTTVersion("5.0")
161+
defer { CocoaMQTTStorage()?.setMQTTVersion("3.1.1") }
162+
163+
let frame = FramePublish(packetFixedHeaderType: FrameType.publish.rawValue, bytes: [0x00, 0x00, 0x03, 0x23, 0x00, 0x01])
164+
XCTAssertEqual(frame?.topic, "")
165+
}
166+
145167
func testFramePubAck() {
146168

147169
var puback = FramePubAck(msgid: 0x1010)

Source/CocoaMQTT.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,4 +794,14 @@ extension CocoaMQTT: CocoaMQTTReaderDelegate {
794794
delegate?.mqttDidReceivePong(self)
795795
didReceivePong(self)
796796
}
797+
798+
func didReceive(_ reader: CocoaMQTTReader, disconnect: FrameDisconnect) {
799+
printWarning("Received DISCONNECT in MQTT 3.1.1 mode, closing socket")
800+
internal_disconnect()
801+
}
802+
803+
func didReceive(_ reader: CocoaMQTTReader, auth: FrameAuth) {
804+
printWarning("Received AUTH in MQTT 3.1.1 mode, closing socket")
805+
internal_disconnect()
806+
}
797807
}

Source/CocoaMQTT5.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -702,13 +702,15 @@ extension CocoaMQTT5: CocoaMQTTSocketDelegate {
702702
extension CocoaMQTT5: CocoaMQTTReaderDelegate {
703703

704704
func didReceive(_ reader: CocoaMQTTReader, disconnect: FrameDisconnect) {
705-
delegate?.mqtt5(self, didReceiveDisconnectReasonCode: disconnect.receiveReasonCode!)
706-
didDisconnectReasonCode(self, disconnect.receiveReasonCode!)
705+
let reasonCode = disconnect.receiveReasonCode ?? .normalDisconnection
706+
delegate?.mqtt5(self, didReceiveDisconnectReasonCode: reasonCode)
707+
didDisconnectReasonCode(self, reasonCode)
707708
}
708709

709710
func didReceive(_ reader: CocoaMQTTReader, auth: FrameAuth) {
710-
delegate?.mqtt5(self, didReceiveAuthReasonCode: auth.receiveReasonCode!)
711-
didAuthReasonCode(self, auth.receiveReasonCode!)
711+
let reasonCode = auth.receiveReasonCode ?? .success
712+
delegate?.mqtt5(self, didReceiveAuthReasonCode: reasonCode)
713+
didAuthReasonCode(self, reasonCode)
712714
}
713715

714716
func didReceive(_ reader: CocoaMQTTReader, connack: FrameConnAck) {

Source/CocoaMQTTReader.swift

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ protocol CocoaMQTTReaderDelegate: AnyObject {
3535
func didReceive(_ reader: CocoaMQTTReader, unsuback: FrameUnsubAck)
3636

3737
func didReceive(_ reader: CocoaMQTTReader, pingresp: FramePingResp)
38+
39+
func didReceive(_ reader: CocoaMQTTReader, disconnect: FrameDisconnect)
40+
41+
func didReceive(_ reader: CocoaMQTTReader, auth: FrameAuth)
3842
}
3943

4044
class CocoaMQTTReader {
@@ -109,8 +113,7 @@ class CocoaMQTTReader {
109113
private func frameReady() {
110114

111115
guard let frameType = FrameType(rawValue: UInt8(header & 0xF0)) else {
112-
printError("Received unknown frame type, header: \(header), data:\(data)")
113-
readHeader()
116+
protocolError("Received unknown frame type, header: \(header), data:\(data)")
114117
return
115118
}
116119

@@ -119,65 +122,95 @@ class CocoaMQTTReader {
119122
switch frameType {
120123
case .connack:
121124
guard let connack = FrameConnAck(packetFixedHeaderType: header, bytes: data) else {
122-
printError("Reader parse \(frameType) failed, data: \(data)")
123-
break
125+
protocolError("Reader parse \(frameType) failed, data: \(data)")
126+
return
124127
}
125128
delegate?.didReceive(self, connack: connack)
126129
case .publish:
127130
guard let publish = FramePublish(packetFixedHeaderType: header, bytes: data) else {
128-
printError("Reader parse \(frameType) failed, data: \(data)")
129-
break
131+
protocolError("Reader parse \(frameType) failed, data: \(data)")
132+
return
130133
}
131134
delegate?.didReceive(self, publish: publish)
132135
case .puback:
133136
guard let puback = FramePubAck(packetFixedHeaderType: header, bytes: data) else {
134-
printError("Reader parse \(frameType) failed, data: \(data)")
135-
break
137+
protocolError("Reader parse \(frameType) failed, data: \(data)")
138+
return
136139
}
137140
delegate?.didReceive(self, puback: puback)
138141
case .pubrec:
139142
guard let pubrec = FramePubRec(packetFixedHeaderType: header, bytes: data) else {
140-
printError("Reader parse \(frameType) failed, data: \(data)")
141-
break
143+
protocolError("Reader parse \(frameType) failed, data: \(data)")
144+
return
142145
}
143146
delegate?.didReceive(self, pubrec: pubrec)
144147
case .pubrel:
145148
guard let pubrel = FramePubRel(packetFixedHeaderType: header, bytes: data) else {
146-
printError("Reader parse \(frameType) failed, data: \(data)")
147-
break
149+
protocolError("Reader parse \(frameType) failed, data: \(data)")
150+
return
148151
}
149152
delegate?.didReceive(self, pubrel: pubrel)
150153
case .pubcomp:
151154
guard let pubcomp = FramePubComp(packetFixedHeaderType: header, bytes: data) else {
152-
printError("Reader parse \(frameType) failed, data: \(data)")
153-
break
155+
protocolError("Reader parse \(frameType) failed, data: \(data)")
156+
return
154157
}
155158
delegate?.didReceive(self, pubcomp: pubcomp)
156159
case .suback:
157160
guard let frame = FrameSubAck(packetFixedHeaderType: header, bytes: data) else {
158-
printError("Reader parse \(frameType) failed, data: \(data)")
159-
break
161+
protocolError("Reader parse \(frameType) failed, data: \(data)")
162+
return
160163
}
161164
delegate?.didReceive(self, suback: frame)
162165
case .unsuback:
163166
guard let frame = FrameUnsubAck(packetFixedHeaderType: header, bytes: data) else {
164-
printError("Reader parse \(frameType) failed, data: \(data)")
165-
break
167+
protocolError("Reader parse \(frameType) failed, data: \(data)")
168+
return
166169
}
167170
delegate?.didReceive(self, unsuback: frame)
168171
case .pingresp:
169172
guard let frame = FramePingResp(packetFixedHeaderType: header, bytes: data) else {
170-
printError("Reader parse \(frameType) failed, data: \(data)")
171-
break
173+
protocolError("Reader parse \(frameType) failed, data: \(data)")
174+
return
172175
}
173176
delegate?.didReceive(self, pingresp: frame)
177+
case .disconnect:
178+
guard isMQTT5ProtocolVersion() else {
179+
protocolError("Reader received MQTT5-only frame \(frameType) in non-MQTT5 mode, data: \(data)")
180+
return
181+
}
182+
guard let frame = FrameDisconnect(packetFixedHeaderType: header, bytes: data) else {
183+
protocolError("Reader parse \(frameType) failed, data: \(data)")
184+
return
185+
}
186+
delegate?.didReceive(self, disconnect: frame)
187+
case .auth:
188+
guard isMQTT5ProtocolVersion() else {
189+
protocolError("Reader received MQTT5-only frame \(frameType) in non-MQTT5 mode, data: \(data)")
190+
return
191+
}
192+
guard let frame = FrameAuth(packetFixedHeaderType: header, bytes: data) else {
193+
protocolError("Reader parse \(frameType) failed, data: \(data)")
194+
return
195+
}
196+
delegate?.didReceive(self, auth: frame)
174197
default:
175-
break
198+
protocolError("Received unsupported frame type \(frameType), data: \(data)")
199+
return
176200
}
177201

178202
readHeader()
179203
}
180204

205+
private func protocolError(_ reason: String) {
206+
printError(reason)
207+
socket.disconnect()
208+
}
209+
210+
private func isMQTT5ProtocolVersion() -> Bool {
211+
return CocoaMQTTStorage()?.queryMQTTVersion() == "5.0"
212+
}
213+
181214
private func reset() {
182215
length = 0
183216
multiply = 1

Source/FrameAuth.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,18 @@ extension FrameAuth {
7070
extension FrameAuth: InitialWithBytes {
7171

7272
init?(packetFixedHeaderType: UInt8, bytes: [UInt8]) {
73-
74-
receiveReasonCode = CocoaMQTTAUTHReasonCode(rawValue: bytes[0])
73+
var protocolVersion = ""
74+
if let storage = CocoaMQTTStorage() {
75+
protocolVersion = storage.queryMQTTVersion()
76+
}
77+
guard protocolVersion == "5.0" else {
78+
return nil
79+
}
80+
if bytes.isEmpty {
81+
receiveReasonCode = .success
82+
} else {
83+
receiveReasonCode = CocoaMQTTAUTHReasonCode(rawValue: bytes[0])
84+
}
7585
}
7686

7787
}

Source/FrameDisconnect.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,13 @@ extension FrameDisconnect: InitialWithBytes {
109109
}
110110

111111
if protocolVersion == "5.0" {
112-
if bytes.count > 0 {
112+
if bytes.isEmpty {
113+
receiveReasonCode = .normalDisconnection
114+
} else {
113115
receiveReasonCode = CocoaMQTTDISCONNECTReasonCode(rawValue: bytes[0])
114116
}
117+
} else {
118+
return nil
115119
}
116120
}
117121

Source/FramePublish.swift

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,14 @@ extension FramePublish: InitialWithBytes {
156156
return nil
157157
}
158158

159-
let len = UInt16(bytes[0]) << 8 + UInt16(bytes[1])
159+
let topicLength = Int(UInt16(bytes[0]) << 8 | UInt16(bytes[1]))
160+
let topicStart = 2
161+
let topicEnd = topicStart + topicLength
160162

161-
// 2 is packetFixedHeaderType length
162-
var pos = 2 + Int(len)
163-
164-
if bytes.count < pos {
163+
if bytes.count < topicEnd {
165164
return nil
166165
}
166+
var pos = topicEnd
167167

168168
// msgid
169169
if (packetFixedHeaderType & 0x06) >> 1 == CocoaMQTTQoS.qos0.rawValue {
@@ -189,6 +189,14 @@ extension FramePublish: InitialWithBytes {
189189
if data.propertyLength != 0 {
190190
pos += data.propertyLength!
191191
}
192+
if pos > bytes.count {
193+
return nil
194+
}
195+
196+
// MQTT 5.0: Topic Name may be empty only when Topic Alias is present.
197+
if data.topic.isEmpty && data.topicAlias == nil {
198+
return nil
199+
}
192200

193201
// MQTT 5.0
194202
self.mqtt5Topic = data.topic
@@ -201,9 +209,13 @@ extension FramePublish: InitialWithBytes {
201209

202210
} else {
203211
// MQTT 3.1.1
204-
if let data = NSString(bytes: [UInt8](bytes[2...(pos-1)]), length: Int(len), encoding: String.Encoding.utf8.rawValue) {
205-
topic = data as String
212+
guard topicLength > 0 else {
213+
return nil
214+
}
215+
guard let recTopic = String(bytes: bytes[topicStart..<topicEnd], encoding: .utf8) else {
216+
return nil
206217
}
218+
topic = recTopic
207219
}
208220

209221
// payload

0 commit comments

Comments
 (0)