Skip to content

Commit 4646740

Browse files
Prohibited using of zero and negative filed number in ProtoNumber and zero field numbers in input bytes (#2766)
- implemented throwing error if decoded field number = 0 - prohibited using of zero and negative filed number in @ProtoNumber annotation - optimized skipping of size delimited fields - removed the creation of an byte array in case of skipping Fixes #2649 Fixes #1566 Co-authored-by: Leonid Startsev <[email protected]>
1 parent 4ca05dd commit 4646740

File tree

7 files changed

+135
-5
lines changed

7 files changed

+135
-5
lines changed

formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/Helpers.kt

+11
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ internal fun SerialDescriptor.extractParameters(index: Int): ProtoDesc {
8787
val annotation = annotations[i]
8888
if (annotation is ProtoNumber) {
8989
protoId = annotation.number
90+
checkFieldNumber(protoId, i, this)
9091
} else if (annotation is ProtoType) {
9192
format = annotation.type
9293
} else if (annotation is ProtoPacked) {
@@ -118,11 +119,21 @@ internal fun extractProtoId(descriptor: SerialDescriptor, index: Int, zeroBasedD
118119
return ID_HOLDER_ONE_OF
119120
} else if (annotation is ProtoNumber) {
120121
result = annotation.number
122+
// 0 or negative numbers are acceptable for enums
123+
if (!zeroBasedDefault) {
124+
checkFieldNumber(result, i, descriptor)
125+
}
121126
}
122127
}
123128
return result
124129
}
125130

131+
private fun checkFieldNumber(fieldNumber: Int, propertyIndex: Int, descriptor: SerialDescriptor) {
132+
if (fieldNumber <= 0) {
133+
throw SerializationException("$fieldNumber is not allowed in ProtoNumber for property '${descriptor.getElementName(propertyIndex)}' of '${descriptor.serialName}', because protobuf supports field numbers in range 1..${Int.MAX_VALUE}")
134+
}
135+
}
136+
126137
internal class ProtobufDecodingException(message: String, e: Throwable? = null) : SerializationException(message, e)
127138

128139
internal expect fun Int.reverseBytes(): Int

formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt

+7-1
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ internal open class ProtobufDecoder(
4545
* If we have reasonably small count of elements, try to build sequential
4646
* array for the fast-path. Fast-path implies that elements are not marked with @ProtoId
4747
* explicitly or are monotonic and incremental (maybe, 1-indexed)
48+
*
49+
* Initialize all elements, because there will always be one extra element as arrays are numbered from 0
50+
* but in protobuf field number starts from 1.
4851
*/
49-
val cache = IntArray(elements + 1)
52+
val cache = IntArray(elements + 1) { -1 }
5053
for (i in 0 until elements) {
5154
val protoId = extractProtoId(descriptor, i, false)
5255
// If any element is marked as ProtoOneOf,
@@ -307,6 +310,9 @@ internal open class ProtobufDecoder(
307310
if (protoId == -1) { // EOF
308311
return elementMarker.nextUnmarkedIndex()
309312
}
313+
if (protoId == 0) {
314+
throw SerializationException("0 is not allowed as the protobuf field number in ${descriptor.serialName}, the input bytes may have been corrupted")
315+
}
310316
val index = getIndexByNum(protoId)
311317
if (index == -1) { // not found
312318
reader.skipElement()

formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufReader.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ internal class ProtobufReader(private val input: ByteArrayInput) {
5959
when (currentType) {
6060
ProtoWireType.VARINT -> readInt(ProtoIntegerType.DEFAULT)
6161
ProtoWireType.i64 -> readLong(ProtoIntegerType.FIXED)
62-
ProtoWireType.SIZE_DELIMITED -> readByteArray()
62+
ProtoWireType.SIZE_DELIMITED -> skipSizeDelimited()
6363
ProtoWireType.i32 -> readInt(ProtoIntegerType.FIXED)
6464
else -> throw ProtobufDecodingException("Unsupported start group or end group wire type: $currentType")
6565
}
@@ -75,6 +75,13 @@ internal class ProtobufReader(private val input: ByteArrayInput) {
7575
return readByteArrayNoTag()
7676
}
7777

78+
fun skipSizeDelimited() {
79+
assertWireType(ProtoWireType.SIZE_DELIMITED)
80+
val length = decode32()
81+
checkLength(length)
82+
input.skipExactNBytes(length)
83+
}
84+
7885
fun readByteArrayNoTag(): ByteArray {
7986
val length = decode32()
8087
checkLength(length)

formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/Streams.kt

+6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ internal class ByteArrayInput(private var array: ByteArray, private val endIndex
3333
return b
3434
}
3535

36+
37+
fun skipExactNBytes(bytesCount: Int) {
38+
ensureEnoughBytes(bytesCount)
39+
position += bytesCount
40+
}
41+
3642
private fun ensureEnoughBytes(bytesCount: Int) {
3743
if (bytesCount > availableBytes) {
3844
throw SerializationException("Unexpected EOF, available $availableBytes bytes, requested: $bytesCount")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.protobuf
6+
7+
8+
import kotlinx.serialization.*
9+
import kotlin.test.*
10+
11+
class InvalidFieldNumberTest {
12+
13+
@Serializable
14+
data class Holder(val value: Int)
15+
16+
@Serializable
17+
data class ListHolder(val value: List<Int>)
18+
19+
@Serializable
20+
data class ZeroProtoNumber(@ProtoNumber(0) val value: Int)
21+
22+
@Serializable
23+
data class NegativeProtoNumber(@ProtoNumber(-5) val value: Int)
24+
25+
@Test
26+
fun testDeserializeZeroInput() {
27+
assertFailsWithMessage<SerializationException>("0 is not allowed as the protobuf field number in kotlinx.serialization.protobuf.InvalidFieldNumberTest.Holder, the input bytes may have been corrupted") {
28+
// first value with field number = 0
29+
val hexString = "000f"
30+
ProtoBuf.decodeFromHexString<Holder>(hexString)
31+
}
32+
}
33+
34+
@Test
35+
fun testDeserializeZeroInputForElement() {
36+
assertFailsWithMessage<SerializationException>("0 is not allowed as the protobuf field number in kotlinx.serialization.protobuf.InvalidFieldNumberTest.ListHolder, the input bytes may have been corrupted") {
37+
// first element with field number = 0
38+
val hexString = "000f"
39+
ProtoBuf.decodeFromHexString<ListHolder>(hexString)
40+
}
41+
}
42+
43+
@Test
44+
fun testSerializeZeroProtoNumber() {
45+
assertFailsWithMessage<SerializationException>("0 is not allowed in ProtoNumber for property 'value' of 'kotlinx.serialization.protobuf.InvalidFieldNumberTest.ZeroProtoNumber', because protobuf supports field numbers in range 1..2147483647") {
46+
ProtoBuf.encodeToHexString(ZeroProtoNumber(42))
47+
}
48+
}
49+
50+
@Test
51+
fun testDeserializeZeroProtoNumber() {
52+
assertFailsWithMessage<SerializationException>("0 is not allowed in ProtoNumber for property 'value' of 'kotlinx.serialization.protobuf.InvalidFieldNumberTest.ZeroProtoNumber', because protobuf supports field numbers in range 1..2147483647") {
53+
ProtoBuf.decodeFromHexString<ZeroProtoNumber>("000f")
54+
}
55+
}
56+
57+
@Test
58+
fun testSerializeNegativeProtoNumber() {
59+
assertFailsWithMessage<SerializationException>("-5 is not allowed in ProtoNumber for property 'value' of 'kotlinx.serialization.protobuf.InvalidFieldNumberTest.NegativeProtoNumber', because protobuf supports field numbers in range 1..2147483647") {
60+
ProtoBuf.encodeToHexString(NegativeProtoNumber(42))
61+
}
62+
}
63+
64+
@Test
65+
fun testDeserializeNegativeProtoNumber() {
66+
assertFailsWithMessage<SerializationException>("-5 is not allowed in ProtoNumber for property 'value' of 'kotlinx.serialization.protobuf.InvalidFieldNumberTest.NegativeProtoNumber', because protobuf supports field numbers in range 1..2147483647") {
67+
ProtoBuf.decodeFromHexString<NegativeProtoNumber>("000f")
68+
}
69+
}
70+
}

formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/PackedArraySerializerTest.kt

+2-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ class PackedArraySerializerTest {
4747

4848
@Serializable
4949
data class PackedStringCarrier(
50-
@ProtoNumber(0)
5150
@ProtoPacked
5251
val s: List<String>
5352
)
@@ -110,12 +109,12 @@ class PackedArraySerializerTest {
110109
@Test
111110
fun testEncodeAnnotatedStringList() {
112111
val obj = PackedStringCarrier(listOf("aaa", "bbb", "ccc"))
113-
val expectedHex = "020361616102036262620203636363"
112+
val expectedHex = "0a036161610a036262620a03636363"
114113
val encodedHex = ProtoBuf.encodeToHexString(obj)
115114
assertEquals(expectedHex, encodedHex)
116115
assertEquals(obj, ProtoBuf.decodeFromHexString<PackedStringCarrier>(expectedHex))
117116

118-
val invalidPackedHex = "020C036161610362626203636363"
117+
val invalidPackedHex = "0a0C036161610362626203636363"
119118
val decoded = ProtoBuf.decodeFromHexString<PackedStringCarrier>(invalidPackedHex)
120119
val invalidString = "\u0003aaa\u0003bbb\u0003ccc"
121120
assertEquals(PackedStringCarrier(listOf(invalidString)), decoded)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.protobuf
6+
7+
8+
import kotlinx.serialization.*
9+
import kotlin.test.*
10+
11+
class SkipFieldsTest {
12+
13+
@Serializable
14+
data class Holder(val value: Int)
15+
16+
@Test
17+
fun testSkipBigFieldNumber() {
18+
// first value with id = 2047 and takes 2 bytes
19+
val hexString = "f87f20082a"
20+
val holder = ProtoBuf.decodeFromHexString<Holder>(hexString)
21+
assertEquals(42, holder.value)
22+
}
23+
24+
@Test
25+
fun testSkipUnknownFiledNumberForString() {
26+
// first value is size delimited (string) with id = 42
27+
val hexString = "d2020c48656c6c6f20576f726c6421082a"
28+
val holder = ProtoBuf.decodeFromHexString<Holder>(hexString)
29+
assertEquals(42, holder.value)
30+
}
31+
}

0 commit comments

Comments
 (0)