fix: harden unknown file TLV parsing

This commit is contained in:
callebtc 2026-07-30 19:33:02 +02:00
parent 46de1a3623
commit 1a792ecfec
2 changed files with 34 additions and 3 deletions

View File

@ -33,7 +33,15 @@ data class BitchatFilePacket(
) {
private enum class TLVType(val v: UByte) {
FILE_NAME(0x01u), FILE_SIZE(0x02u), MIME_TYPE(0x03u), CONTENT(0x04u);
companion object { fun from(value: UByte) = values().find { it.v == value } }
companion object {
fun from(value: UByte): TLVType? = when (value) {
FILE_NAME.v -> FILE_NAME
FILE_SIZE.v -> FILE_SIZE
MIME_TYPE.v -> MIME_TYPE
CONTENT.v -> CONTENT
else -> null
}
}
}
fun encode(): ByteArray? {
@ -98,7 +106,11 @@ data class BitchatFilePacket(
var mime: String? = null
var contentBytes: ByteArray? = null
var skippedUnknownTLVs = 0
while (off + 3 <= data.size) { // minimum TLV header size (type + 2 bytes length)
while (off < data.size) {
// Every TLV needs at least a type and a 2-byte length.
// Reject a truncated trailing header instead of silently
// accepting it, matching the iOS decoder.
if (data.size - off < 3) return null
// A null `t` is an unknown tag: read its length like any
// other 2-byte TLV and skip its value, matching iOS.
val t = TLVType.from(data[off].toUByte())
@ -163,4 +175,3 @@ data class BitchatFilePacket(
}
}
}

View File

@ -5,6 +5,7 @@ import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.BitchatMessageType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@ -201,6 +202,25 @@ class FileTransferTest {
assertEquals(content.size, decoded.content.size)
}
@Test
fun `decode should reject an incomplete TLV header after an unknown extension`() {
// Given: a valid file and unknown extension followed by either only a
// tag or a tag plus one length byte.
val content = ByteArray(8) { 0x2A }
val fileName = "truncated.bin".toByteArray(Charsets.UTF_8)
val buf = ByteBuffer.allocate(
(1 + 2 + fileName.size) + (1 + 4 + content.size) + (1 + 2)
).order(ByteOrder.BIG_ENDIAN)
buf.put(0x01.toByte()); buf.putShort(fileName.size.toShort()); buf.put(fileName)
buf.put(0x04.toByte()); buf.putInt(content.size); buf.put(content)
buf.put(0x05.toByte()); buf.putShort(0)
val packetWithUnknownExtension = buf.array()
// When/Then: Android rejects the same incomplete tails that iOS does.
assertNull(BitchatFilePacket.decode(packetWithUnknownExtension + byteArrayOf(0x06)))
assertNull(BitchatFilePacket.decode(packetWithUnknownExtension + byteArrayOf(0x06, 0x00)))
}
@Test
fun `decode should handle a packet padded with many zero-length unknown TLVs`() {
// Given: the cheapest padding a peer can send — a zero-length unknown