diff --git a/bitchat/Protocols/BinaryProtocol.swift b/bitchat/Protocols/BinaryProtocol.swift index 5a858ae4..2d78263a 100644 --- a/bitchat/Protocols/BinaryProtocol.swift +++ b/bitchat/Protocols/BinaryProtocol.swift @@ -212,24 +212,30 @@ struct BinaryProtocol { // Remove padding first let unpaddedData = MessagePadding.unpad(data) - + // Minimum size check: header + senderID guard unpaddedData.count >= headerSize + senderIDSize else { return nil } var offset = 0 - // Header + // Header parsing with bounds checks + guard offset + 1 <= unpaddedData.count else { return nil } let version = unpaddedData[offset]; offset += 1 + // Check if version is supported guard ProtocolVersion.isSupported(version) else { - // Log unsupported version for debugging return nil } + + guard offset + 1 <= unpaddedData.count else { return nil } let type = unpaddedData[offset]; offset += 1 + + guard offset + 1 <= unpaddedData.count else { return nil } let ttl = unpaddedData[offset]; offset += 1 - // Timestamp + // Timestamp - need 8 bytes + guard offset + 8 <= unpaddedData.count else { return nil } let timestampData = unpaddedData[offset..= expectedSize else { - return nil - } - - // SenderID + // SenderID - need 8 bytes + guard offset + senderIDSize <= unpaddedData.count else { return nil } let senderID = unpaddedData[offset..= 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..= 0 && originalSize <= 1048576 else { return nil } // Max 1MB - // 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..= 0 && offset + Int(payloadLength) <= unpaddedData.count else { + return nil + } payload = unpaddedData[offset..() @@ -150,8 +151,8 @@ final class BinaryProtocolTests: XCTestCase { XCTAssertEqual(String(data: decodedPacket.payload, encoding: .utf8), payload) } - // Different payload sizes should result in different padded sizes - XCTAssertGreaterThan(encodedSizes.count, 1) + // Different payload sizes should result in at least 2 different padded sizes + XCTAssertGreaterThanOrEqual(encodedSizes.count, 2, "Expected at least 2 different padded sizes, got \(encodedSizes)") } // MARK: - Message Encoding/Decoding Tests @@ -312,4 +313,259 @@ final class BinaryProtocolTests: XCTestCase { // Should fail to decode 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 + } } \ No newline at end of file