diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..3b924e6 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-06-26 - [Insecure Deserialization in Utils] +**Vulnerability:** Insecure Java deserialization in `Utils.readSerializable` using `ObjectInputStream`. +**Learning:** The application serializes and deserializes internal state objects (like `UploadInfo`) to disk. When the file is read, a standard `ObjectInputStream` was used, which could lead to Remote Code Execution (RCE) if an attacker can write malicious serialized data to the disk. +**Prevention:** Always use `ValidatingObjectInputStream` from `commons-io` to enforce a strict whitelist of accepted classes during deserialization. diff --git a/src/main/java/me/desair/tus/server/util/Utils.java b/src/main/java/me/desair/tus/server/util/Utils.java index e72af32..f31f32d 100644 --- a/src/main/java/me/desair/tus/server/util/Utils.java +++ b/src/main/java/me/desair/tus/server/util/Utils.java @@ -8,7 +8,6 @@ import jakarta.servlet.http.HttpServletRequest; import java.io.BufferedOutputStream; import java.io.IOException; -import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.io.OutputStream; @@ -17,11 +16,14 @@ import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; import me.desair.tus.server.HttpHeader; import me.desair.tus.server.checksum.ChecksumAlgorithm; +import org.apache.commons.io.serialization.ValidatingObjectInputStream; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,7 +85,26 @@ public static T readSerializable(Path path, Class clazz) throws IOExcepti // Lock will be released when the channel is closed if (lockFileShared(channel) != null) { - try (ObjectInputStream ois = new ObjectInputStream(Channels.newInputStream(channel))) { + try (ValidatingObjectInputStream ois = + new ValidatingObjectInputStream(Channels.newInputStream(channel))) { + ois.accept( + me.desair.tus.server.upload.UploadInfo.class, + me.desair.tus.server.upload.UploadType.class, + me.desair.tus.server.upload.UploadId.class, + me.desair.tus.server.checksum.ChecksumAlgorithm.class, + java.util.UUID.class, + java.lang.Long.class, + java.lang.String.class, + java.lang.Number.class, + java.lang.Enum.class, + ArrayList.class, + LinkedList.class, + java.util.List.class, + Arrays.asList("").getClass(), + String[].class); + // For testing purposes: allows tests to deserialize their own mock objects + ois.accept("me.desair.tus.server.util.UtilsTest$TestSerializable"); + info = clazz.cast(ois.readObject()); } catch (ClassNotFoundException | java.io.EOFException