Skip to content

Commit 485e30b

Browse files
authored
Deserialization: verify manifest size (#336)
* Deserialization: verify manifest size re https://github.com/joernio/flatgraph/security/advisories/GHSA-jqmx-3x2p-69vh n.b. it is not our goal to have a "safe" file format, and we need to document that properly. But adding some more straightforward checks doesn't harm. * clarify comment * also check for manifestSize > Int.MaxValue
1 parent 1e0bb23 commit 485e30b

File tree

2 files changed

+58
-6
lines changed

2 files changed

+58
-6
lines changed

core/src/main/scala/flatgraph/storage/Deserialization.scala

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,9 @@ object Deserialization {
173173
}
174174

175175
def readManifest(channel: FileChannel): ujson.Value = {
176-
if (channel.size() < HeaderSize)
177-
throw new DeserializationException(s"corrupt file, expected at least $HeaderSize bytes, but only found ${channel.size()}")
176+
val fileSize = channel.size()
177+
if (fileSize < HeaderSize)
178+
throw new DeserializationException(s"corrupt file: expected at least $HeaderSize bytes, but only found ${channel.size()}")
178179

179180
val header = ByteBuffer.allocate(HeaderSize).order(ByteOrder.LITTLE_ENDIAN)
180181
var readBytes = 0
@@ -185,21 +186,26 @@ object Deserialization {
185186

186187
val headerBytes = new Array[Byte](Keys.Header.length)
187188
header.get(headerBytes)
188-
if (!Arrays.equals(headerBytes, Keys.Header))
189+
if (!Arrays.equals(headerBytes, Keys.Header)) {
189190
throw new DeserializationException(
190191
s"expected header '$MagicBytesString' (`${Keys.Header.mkString("")}`), but found '${headerBytes.mkString("")}'"
191192
)
193+
}
192194

193195
val manifestOffset = header.getLong()
194196
val manifestSize = channel.size() - manifestOffset
195-
val manifestBytes = ByteBuffer.allocate(manifestSize.toInt)
197+
if (manifestSize > fileSize)
198+
throw new DeserializationException(s"corrupt file: manifest size ($manifestSize) cannot be larger than the file's size ($fileSize)")
199+
if (manifestSize > Int.MaxValue)
200+
throw new DeserializationException(s"corrupt file: unreasonably large manifest size ($manifestSize)... aborting")
201+
202+
val manifestBytes = ByteBuffer.allocate(manifestSize.toInt)
196203
readBytes = 0
197204
while (readBytes < manifestSize) {
198205
readBytes += channel.read(manifestBytes, readBytes + manifestOffset)
199206
}
200207
manifestBytes.flip()
201208
ujson.read(manifestBytes)
202-
203209
}
204210

205211
private def readPool(manifest: GraphItem, fileChannel: FileChannel, zstdCtx: ZstdWrapper.ZstdCtx): Array[String] = {

core/src/test/scala/flatgraph/SerializationTests.scala

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package flatgraph
22

33
import flatgraph.misc.DebugDump.debugDump
4+
import flatgraph.storage.Deserialization.DeserializationException
45
import flatgraph.storage.{Deserialization, Serialization}
56
import org.scalatest.matchers.should.Matchers
67
import org.scalatest.wordspec.AnyWordSpec
78

8-
import java.nio.file.Files
9+
import java.nio.file.{Files, Path}
10+
import java.io.RandomAccessFile
11+
import java.nio.ByteBuffer
12+
import java.nio.ByteOrder
13+
import scala.util.Using
914

1015
class SerializationTests extends AnyWordSpec with Matchers {
1116

@@ -40,4 +45,45 @@ class SerializationTests extends AnyWordSpec with Matchers {
4045
originalDump shouldBe newDump
4146
}
4247

48+
/* Show that we're no longer vulnerable to the 'denial of service attack by manipulating the manifest'
49+
* issue filed here: https://github.com/joernio/flatgraph/security/advisories/GHSA-jqmx-3x2p-69vh
50+
* Note that we cannot prevent all potential 'small flatgraph file leads to OOM error' attacks.
51+
* Always treat untrusted files with precaution...
52+
*/
53+
"is no longer vulnerable to manifest size attack" in {
54+
val schema = TestSchema.make(1, 0)
55+
val graph = Graph(schema)
56+
val diff = DiffGraphBuilder(schema).addNode(new GenericDNode(0))
57+
DiffGraphApplier.applyDiff(graph, diff)
58+
59+
val storagePath = Files.createTempFile(s"flatgraph-${getClass.getSimpleName}", "fg")
60+
Serialization.writeGraph(graph, storagePath)
61+
patchFile(storagePath)
62+
63+
// when the vulnerability was reported, the following line raised a:
64+
// `java.lang.OutOfMemoryError: Requested array size exceeds VM limit`
65+
intercept[DeserializationException] {
66+
Deserialization.readGraph(storagePath, Option(graph.schema))
67+
}.getMessage should include("corrupt file: manifest size")
68+
}
69+
70+
/** manipulate file as detailed in https: //github.com/joernio/flatgraph/security/advisories/GHSA-jqmx-3x2p-69vh */
71+
private def patchFile(path: Path): Unit = {
72+
Using.resource(new RandomAccessFile(path.toFile, "rw")) { file =>
73+
// Seek to end and get file size
74+
file.seek(file.length())
75+
val fileSize = file.getFilePointer
76+
77+
// Calculate malicious offset
78+
val maliciousOffset = fileSize - 2147483647L
79+
80+
// Seek to position 8 and write the offset as little-endian long
81+
file.seek(8)
82+
val buffer = ByteBuffer.allocate(8)
83+
buffer.order(ByteOrder.LITTLE_ENDIAN)
84+
buffer.putLong(maliciousOffset)
85+
file.write(buffer.array())
86+
}
87+
}
88+
4389
}

0 commit comments

Comments
 (0)