mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 17:45:19 +00:00
Fix crash in BinaryProtocol.decode due to out-of-bounds data access
- Add comprehensive bounds checking before all data access operations - Validate payload lengths don't exceed available data - Protect against integer overflow in size calculations - Handle malformed compressed packets gracefully - Add extensive test coverage for edge cases This fixes crashes when receiving malformed Bluetooth packets that claim payload sizes larger than the actual data available.
This commit is contained in:
@@ -212,24 +212,30 @@ struct BinaryProtocol {
|
|||||||
// Remove padding first
|
// Remove padding first
|
||||||
let unpaddedData = MessagePadding.unpad(data)
|
let unpaddedData = MessagePadding.unpad(data)
|
||||||
|
|
||||||
|
// Minimum size check: header + senderID
|
||||||
guard unpaddedData.count >= headerSize + senderIDSize else {
|
guard unpaddedData.count >= headerSize + senderIDSize else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset = 0
|
var offset = 0
|
||||||
|
|
||||||
// Header
|
// Header parsing with bounds checks
|
||||||
|
guard offset + 1 <= unpaddedData.count else { return nil }
|
||||||
let version = unpaddedData[offset]; offset += 1
|
let version = unpaddedData[offset]; offset += 1
|
||||||
|
|
||||||
// Check if version is supported
|
// Check if version is supported
|
||||||
guard ProtocolVersion.isSupported(version) else {
|
guard ProtocolVersion.isSupported(version) else {
|
||||||
// Log unsupported version for debugging
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard offset + 1 <= unpaddedData.count else { return nil }
|
||||||
let type = unpaddedData[offset]; offset += 1
|
let type = unpaddedData[offset]; offset += 1
|
||||||
|
|
||||||
|
guard offset + 1 <= unpaddedData.count else { return nil }
|
||||||
let ttl = unpaddedData[offset]; offset += 1
|
let ttl = unpaddedData[offset]; offset += 1
|
||||||
|
|
||||||
// Timestamp
|
// Timestamp - need 8 bytes
|
||||||
|
guard offset + 8 <= unpaddedData.count else { return nil }
|
||||||
let timestampData = unpaddedData[offset..<offset+8]
|
let timestampData = unpaddedData[offset..<offset+8]
|
||||||
let timestamp = timestampData.reduce(0) { result, byte in
|
let timestamp = timestampData.reduce(0) { result, byte in
|
||||||
(result << 8) | UInt64(byte)
|
(result << 8) | UInt64(byte)
|
||||||
@@ -237,73 +243,93 @@ struct BinaryProtocol {
|
|||||||
offset += 8
|
offset += 8
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
|
guard offset + 1 <= unpaddedData.count else { return nil }
|
||||||
let flags = unpaddedData[offset]; offset += 1
|
let flags = unpaddedData[offset]; offset += 1
|
||||||
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
||||||
let hasSignature = (flags & Flags.hasSignature) != 0
|
let hasSignature = (flags & Flags.hasSignature) != 0
|
||||||
let isCompressed = (flags & Flags.isCompressed) != 0
|
let isCompressed = (flags & Flags.isCompressed) != 0
|
||||||
|
|
||||||
// Payload length
|
// Payload length - need 2 bytes
|
||||||
|
guard offset + 2 <= unpaddedData.count else { return nil }
|
||||||
let payloadLengthData = unpaddedData[offset..<offset+2]
|
let payloadLengthData = unpaddedData[offset..<offset+2]
|
||||||
let payloadLength = payloadLengthData.reduce(0) { result, byte in
|
let payloadLength = payloadLengthData.reduce(0) { result, byte in
|
||||||
(result << 8) | UInt16(byte)
|
(result << 8) | UInt16(byte)
|
||||||
}
|
}
|
||||||
offset += 2
|
offset += 2
|
||||||
|
|
||||||
// Calculate expected total size
|
// Validate payloadLength is reasonable (prevent integer overflow)
|
||||||
var expectedSize = headerSize + senderIDSize + Int(payloadLength)
|
guard payloadLength <= 65535 else { return nil }
|
||||||
if hasRecipient {
|
|
||||||
expectedSize += recipientIDSize
|
|
||||||
}
|
|
||||||
if hasSignature {
|
|
||||||
expectedSize += signatureSize
|
|
||||||
}
|
|
||||||
|
|
||||||
guard unpaddedData.count >= expectedSize else {
|
// SenderID - need 8 bytes
|
||||||
return nil
|
guard offset + senderIDSize <= unpaddedData.count else { return nil }
|
||||||
}
|
|
||||||
|
|
||||||
// SenderID
|
|
||||||
let senderID = unpaddedData[offset..<offset+senderIDSize]
|
let senderID = unpaddedData[offset..<offset+senderIDSize]
|
||||||
offset += senderIDSize
|
offset += senderIDSize
|
||||||
|
|
||||||
// RecipientID
|
// RecipientID if present
|
||||||
var recipientID: Data?
|
var recipientID: Data?
|
||||||
if hasRecipient {
|
if hasRecipient {
|
||||||
|
guard offset + recipientIDSize <= unpaddedData.count else { return nil }
|
||||||
recipientID = unpaddedData[offset..<offset+recipientIDSize]
|
recipientID = unpaddedData[offset..<offset+recipientIDSize]
|
||||||
offset += recipientIDSize
|
offset += recipientIDSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payload
|
// Payload handling with comprehensive bounds checking
|
||||||
let payload: Data
|
let payload: Data
|
||||||
if isCompressed {
|
if isCompressed {
|
||||||
// First 2 bytes are original size
|
// Compressed payload needs at least 2 bytes for original size
|
||||||
guard Int(payloadLength) >= 2 else { return nil }
|
guard Int(payloadLength) >= 2 else { return nil }
|
||||||
|
|
||||||
|
// Check we have enough data for the original size prefix
|
||||||
|
guard offset + 2 <= unpaddedData.count else { return nil }
|
||||||
let originalSizeData = unpaddedData[offset..<offset+2]
|
let originalSizeData = unpaddedData[offset..<offset+2]
|
||||||
let originalSize = Int(originalSizeData.reduce(0) { result, byte in
|
let originalSize = Int(originalSizeData.reduce(0) { result, byte in
|
||||||
(result << 8) | UInt16(byte)
|
(result << 8) | UInt16(byte)
|
||||||
})
|
})
|
||||||
offset += 2
|
offset += 2
|
||||||
|
|
||||||
// Compressed payload
|
// Validate original size is reasonable
|
||||||
let compressedPayload = unpaddedData[offset..<offset+Int(payloadLength)-2]
|
guard originalSize >= 0 && originalSize <= 1048576 else { return nil } // Max 1MB
|
||||||
offset += Int(payloadLength) - 2
|
|
||||||
|
|
||||||
// Decompress
|
// Check we have enough data for the compressed payload
|
||||||
|
let compressedPayloadSize = Int(payloadLength) - 2
|
||||||
|
guard compressedPayloadSize >= 0 && offset + compressedPayloadSize <= unpaddedData.count else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let compressedPayload = unpaddedData[offset..<offset+compressedPayloadSize]
|
||||||
|
offset += compressedPayloadSize
|
||||||
|
|
||||||
|
// Decompress with error handling
|
||||||
guard let decompressedPayload = CompressionUtil.decompress(compressedPayload, originalSize: originalSize) else {
|
guard let decompressedPayload = CompressionUtil.decompress(compressedPayload, originalSize: originalSize) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify decompressed size matches expected
|
||||||
|
guard decompressedPayload.count == originalSize else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
payload = decompressedPayload
|
payload = decompressedPayload
|
||||||
} else {
|
} else {
|
||||||
|
// Uncompressed payload
|
||||||
|
guard Int(payloadLength) >= 0 && offset + Int(payloadLength) <= unpaddedData.count else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
payload = unpaddedData[offset..<offset+Int(payloadLength)]
|
payload = unpaddedData[offset..<offset+Int(payloadLength)]
|
||||||
offset += Int(payloadLength)
|
offset += Int(payloadLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signature
|
// Signature if present
|
||||||
var signature: Data?
|
var signature: Data?
|
||||||
if hasSignature {
|
if hasSignature {
|
||||||
|
guard offset + signatureSize <= unpaddedData.count else { return nil }
|
||||||
signature = unpaddedData[offset..<offset+signatureSize]
|
signature = unpaddedData[offset..<offset+signatureSize]
|
||||||
|
offset += signatureSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final validation: ensure we haven't gone past the end
|
||||||
|
guard offset <= unpaddedData.count else { return nil }
|
||||||
|
|
||||||
return BitchatPacket(
|
return BitchatPacket(
|
||||||
type: type,
|
type: type,
|
||||||
senderID: senderID,
|
senderID: senderID,
|
||||||
|
|||||||
@@ -121,8 +121,9 @@ final class BinaryProtocolTests: XCTestCase {
|
|||||||
func testMessagePadding() throws {
|
func testMessagePadding() throws {
|
||||||
let payloads = [
|
let payloads = [
|
||||||
"Short",
|
"Short",
|
||||||
"This is a medium length message for testing",
|
String(repeating: "Medium length message content ", count: 10), // ~300 bytes
|
||||||
TestConstants.testLongMessage
|
String(repeating: "Long message content that should exceed the 512 byte limit ", count: 20), // ~1200+ bytes
|
||||||
|
String(repeating: "Very long message content that should definitely exceed the 2048 byte limit for sure ", count: 30) // ~2700+ bytes
|
||||||
]
|
]
|
||||||
|
|
||||||
var encodedSizes = Set<Int>()
|
var encodedSizes = Set<Int>()
|
||||||
@@ -150,8 +151,8 @@ final class BinaryProtocolTests: XCTestCase {
|
|||||||
XCTAssertEqual(String(data: decodedPacket.payload, encoding: .utf8), payload)
|
XCTAssertEqual(String(data: decodedPacket.payload, encoding: .utf8), payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Different payload sizes should result in different padded sizes
|
// Different payload sizes should result in at least 2 different padded sizes
|
||||||
XCTAssertGreaterThan(encodedSizes.count, 1)
|
XCTAssertGreaterThanOrEqual(encodedSizes.count, 2, "Expected at least 2 different padded sizes, got \(encodedSizes)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Message Encoding/Decoding Tests
|
// MARK: - Message Encoding/Decoding Tests
|
||||||
@@ -312,4 +313,259 @@ final class BinaryProtocolTests: XCTestCase {
|
|||||||
// Should fail to decode
|
// Should fail to decode
|
||||||
XCTAssertNil(BinaryProtocol.decode(encoded))
|
XCTAssertNil(BinaryProtocol.decode(encoded))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Bounds Checking Tests (Crash Prevention)
|
||||||
|
|
||||||
|
func testMalformedPacketWithInvalidPayloadLength() throws {
|
||||||
|
// Test the specific crash scenario: payloadLength = 193 (0xc1) but only 30 bytes available
|
||||||
|
var malformedData = Data()
|
||||||
|
|
||||||
|
// Valid header (13 bytes)
|
||||||
|
malformedData.append(1) // version
|
||||||
|
malformedData.append(1) // type
|
||||||
|
malformedData.append(10) // ttl
|
||||||
|
|
||||||
|
// Timestamp (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
malformedData.append(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
malformedData.append(0) // flags (no recipient, no signature, not compressed)
|
||||||
|
|
||||||
|
// Invalid payload length: 193 (0x00c1) but we'll only provide 8 bytes total data
|
||||||
|
malformedData.append(0x00) // high byte
|
||||||
|
malformedData.append(0xc1) // low byte (193)
|
||||||
|
|
||||||
|
// SenderID (8 bytes) - this brings us to 21 bytes total
|
||||||
|
for _ in 0..<8 {
|
||||||
|
malformedData.append(0x01)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only provide 8 more bytes instead of the claimed 193
|
||||||
|
for _ in 0..<8 {
|
||||||
|
malformedData.append(0x02)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total data is now 30 bytes, but payloadLength claims 193
|
||||||
|
XCTAssertEqual(malformedData.count, 30)
|
||||||
|
|
||||||
|
// This should not crash - should return nil gracefully
|
||||||
|
let result = BinaryProtocol.decode(malformedData)
|
||||||
|
XCTAssertNil(result, "Malformed packet with invalid payload length should return nil, not crash")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTruncatedPacketHandling() throws {
|
||||||
|
// Test various truncation scenarios
|
||||||
|
let packet = TestHelpers.createTestPacket()
|
||||||
|
guard let validEncoded = BinaryProtocol.encode(packet) else {
|
||||||
|
XCTFail("Failed to encode test packet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test truncation at various points
|
||||||
|
let truncationPoints = [0, 5, 10, 15, 20, 25]
|
||||||
|
|
||||||
|
for point in truncationPoints {
|
||||||
|
let truncated = validEncoded.prefix(point)
|
||||||
|
let result = BinaryProtocol.decode(truncated)
|
||||||
|
XCTAssertNil(result, "Truncated packet at \(point) bytes should return nil, not crash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMalformedCompressedPacket() throws {
|
||||||
|
// Test compressed packet with invalid original size
|
||||||
|
var malformedData = Data()
|
||||||
|
|
||||||
|
// Valid header
|
||||||
|
malformedData.append(1) // version
|
||||||
|
malformedData.append(1) // type
|
||||||
|
malformedData.append(10) // ttl
|
||||||
|
|
||||||
|
// Timestamp (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
malformedData.append(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
malformedData.append(0x04) // flags: isCompressed = true
|
||||||
|
|
||||||
|
// Small payload length that's insufficient for compression
|
||||||
|
malformedData.append(0x00) // high byte
|
||||||
|
malformedData.append(0x01) // low byte (1 byte - insufficient for 2-byte original size)
|
||||||
|
|
||||||
|
// SenderID (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
malformedData.append(0x01)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only 1 byte of "compressed" data (should need at least 2 for original size)
|
||||||
|
malformedData.append(0x99)
|
||||||
|
|
||||||
|
// Should handle this gracefully
|
||||||
|
let result = BinaryProtocol.decode(malformedData)
|
||||||
|
XCTAssertNil(result, "Malformed compressed packet should return nil, not crash")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExcessivelyLargePayloadLength() throws {
|
||||||
|
// Test packet claiming extremely large payload
|
||||||
|
var malformedData = Data()
|
||||||
|
|
||||||
|
// Valid header
|
||||||
|
malformedData.append(1) // version
|
||||||
|
malformedData.append(1) // type
|
||||||
|
malformedData.append(10) // ttl
|
||||||
|
|
||||||
|
// Timestamp (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
malformedData.append(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
malformedData.append(0) // flags
|
||||||
|
|
||||||
|
// Maximum payload length (65535)
|
||||||
|
malformedData.append(0xFF) // high byte
|
||||||
|
malformedData.append(0xFF) // low byte
|
||||||
|
|
||||||
|
// SenderID (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
malformedData.append(0x01)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide only a tiny amount of actual data
|
||||||
|
malformedData.append(contentsOf: [0x01, 0x02, 0x03])
|
||||||
|
|
||||||
|
// Should handle this gracefully without trying to allocate massive amounts of memory
|
||||||
|
let result = BinaryProtocol.decode(malformedData)
|
||||||
|
XCTAssertNil(result, "Packet with excessive payload length should return nil, not crash")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCompressedPacketWithInvalidOriginalSize() throws {
|
||||||
|
// Test compressed packet with unreasonable original size
|
||||||
|
var malformedData = Data()
|
||||||
|
|
||||||
|
// Valid header
|
||||||
|
malformedData.append(1) // version
|
||||||
|
malformedData.append(1) // type
|
||||||
|
malformedData.append(10) // ttl
|
||||||
|
|
||||||
|
// Timestamp (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
malformedData.append(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
malformedData.append(0x04) // flags: isCompressed = true
|
||||||
|
|
||||||
|
// Reasonable payload length
|
||||||
|
malformedData.append(0x00) // high byte
|
||||||
|
malformedData.append(0x10) // low byte (16 bytes)
|
||||||
|
|
||||||
|
// SenderID (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
malformedData.append(0x01)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original size claiming to be extremely large (2MB)
|
||||||
|
malformedData.append(0x20) // high byte of original size
|
||||||
|
malformedData.append(0x00) // low byte of original size (0x2000 = 8192, but let's make it larger with more bytes)
|
||||||
|
|
||||||
|
// Add more bytes to make it claim larger size - but this will be invalid
|
||||||
|
// because our validation should catch unreasonable sizes
|
||||||
|
malformedData.append(contentsOf: [0x01, 0x02, 0x03, 0x04]) // Some compressed data
|
||||||
|
|
||||||
|
// Pad to match payload length
|
||||||
|
while malformedData.count < 21 + 16 { // header + senderID + payload
|
||||||
|
malformedData.append(0x00)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = BinaryProtocol.decode(malformedData)
|
||||||
|
XCTAssertNil(result, "Compressed packet with invalid original size should return nil, not crash")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMaliciousPacketWithIntegerOverflow() throws {
|
||||||
|
// Test packet designed to cause integer overflow
|
||||||
|
var maliciousData = Data()
|
||||||
|
|
||||||
|
// Valid header
|
||||||
|
maliciousData.append(1) // version
|
||||||
|
maliciousData.append(1) // type
|
||||||
|
maliciousData.append(10) // ttl
|
||||||
|
|
||||||
|
// Timestamp (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
maliciousData.append(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set flags to have recipient and signature (increase expected size)
|
||||||
|
maliciousData.append(0x03) // hasRecipient | hasSignature
|
||||||
|
|
||||||
|
// Very large payload length
|
||||||
|
maliciousData.append(0xFF) // high byte
|
||||||
|
maliciousData.append(0xFE) // low byte (65534)
|
||||||
|
|
||||||
|
// SenderID (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
maliciousData.append(0x01)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecipientID (8 bytes - required due to flag)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
maliciousData.append(0x02)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide minimal payload data - should trigger bounds check failure
|
||||||
|
maliciousData.append(contentsOf: [0x01, 0x02])
|
||||||
|
|
||||||
|
// Should handle gracefully without integer overflow issues
|
||||||
|
let result = BinaryProtocol.decode(maliciousData)
|
||||||
|
XCTAssertNil(result, "Malicious packet designed for integer overflow should return nil, not crash")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPartialHeaderData() throws {
|
||||||
|
// Test packets with incomplete headers
|
||||||
|
let headerSizes = [0, 1, 5, 10, 12] // Various incomplete header sizes
|
||||||
|
|
||||||
|
for size in headerSizes {
|
||||||
|
let partialData = Data(repeating: 0x01, count: size)
|
||||||
|
let result = BinaryProtocol.decode(partialData)
|
||||||
|
XCTAssertNil(result, "Partial header data (\(size) bytes) should return nil, not crash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBoundaryConditions() throws {
|
||||||
|
// Test exact boundary conditions
|
||||||
|
let packet = TestHelpers.createTestPacket()
|
||||||
|
guard let validEncoded = BinaryProtocol.encode(packet) else {
|
||||||
|
XCTFail("Failed to encode test packet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove several bytes to ensure it fails - should fail gracefully
|
||||||
|
let truncated = validEncoded.dropLast(10)
|
||||||
|
let result = BinaryProtocol.decode(truncated)
|
||||||
|
XCTAssertNil(result, "Truncated packet should return nil, not crash")
|
||||||
|
|
||||||
|
// Test minimum valid size - create a valid minimal packet
|
||||||
|
var minData = Data()
|
||||||
|
minData.append(1) // version
|
||||||
|
minData.append(1) // type
|
||||||
|
minData.append(10) // ttl
|
||||||
|
|
||||||
|
// Timestamp (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
minData.append(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
minData.append(0) // flags (no optional fields)
|
||||||
|
minData.append(0) // payload length high byte
|
||||||
|
minData.append(0) // payload length low byte (0 payload)
|
||||||
|
|
||||||
|
// SenderID (8 bytes)
|
||||||
|
for _ in 0..<8 {
|
||||||
|
minData.append(0x01)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should be exactly the minimum size and should decode without crashing
|
||||||
|
let minResult = BinaryProtocol.decode(minData)
|
||||||
|
// The important thing is no crash occurs - result might be nil or valid
|
||||||
|
// We don't assert the result, just that no crash happens
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user