From 0f5e46f558290494b5e68d248eb165662d376566 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:07:48 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20Path=20Traversal=20in=20AbstractDiskBasedService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: CRITICAL 💡 Vulnerability: Path traversal vulnerability in disk storage component where upload IDs (user input) were resolved without checking bounds. 🎯 Impact: Attackers could potentially read or write files outside the intended storage directory by providing an upload ID like `../../../etc/passwd`. 🔧 Fix: Normalized the resolved path and verified it starts with the normalized storage path. ✅ Verification: Ran `mvn clean test` and `mvn com.spotify.fmt:fmt-maven-plugin:format` to ensure fix works and formatting passes. Co-authored-by: tomdesair <14034630+tomdesair@users.noreply.github.com> --- .jules/sentinel.md | 7 +++++++ .../tus/server/upload/disk/AbstractDiskBasedService.java | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..e80d8de --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,7 @@ +## 2026-06-23 - Path Traversal Vulnerability in Disk Storage + +**Vulnerability:** The application allowed path traversal in the disk storage component when resolving upload IDs. Specifically, `storagePath.resolve(id.toString())` was used without checking if the resulting path was still within the intended storage directory. + +**Learning:** When resolving paths using user-controlled input (like an upload ID), it is insufficient to simply rely on `Path.resolve`. If the input contains `../` or absolute paths, it could escape the intended boundaries. + +**Prevention:** Always normalize the resolved path and explicitly check that it starts with the normalized base directory using `uploadPath.normalize().startsWith(storagePath.normalize())`. Throw an exception if the check fails. diff --git a/src/main/java/me/desair/tus/server/upload/disk/AbstractDiskBasedService.java b/src/main/java/me/desair/tus/server/upload/disk/AbstractDiskBasedService.java index 0c12472..6250332 100644 --- a/src/main/java/me/desair/tus/server/upload/disk/AbstractDiskBasedService.java +++ b/src/main/java/me/desair/tus/server/upload/disk/AbstractDiskBasedService.java @@ -37,7 +37,12 @@ protected Path getPathInStorageDirectory(UploadId id) { if (id == null) { return null; } else { - return storagePath.resolve(id.toString()); + Path uploadPath = storagePath.resolve(id.toString()).normalize(); + if (!uploadPath.startsWith(storagePath.normalize())) { + throw new IllegalArgumentException( + "The upload ID cannot point to a path outside the storage directory"); + } + return uploadPath; } } From b915221817f3390aa077212b72ef8a6f897628b6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 12:23:11 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20Path=20Traversal=20in=20AbstractDiskBasedService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: CRITICAL 💡 Vulnerability: Path traversal vulnerability in disk storage component where upload IDs (user input) were resolved without checking bounds. 🎯 Impact: Attackers could potentially read or write files outside the intended storage directory by providing an upload ID like `../../../etc/passwd`. 🔧 Fix: Normalized the resolved path and verified it starts with the normalized storage path. Included a unit test to prevent regression. ✅ Verification: Ran `mvn clean test` and `mvn com.spotify.fmt:fmt-maven-plugin:format` to ensure fix works and formatting passes. Co-authored-by: tomdesair <14034630+tomdesair@users.noreply.github.com> --- .../upload/disk/AbstractDiskBasedService.java | 8 ++- .../disk/AbstractDiskBasedServiceTest.java | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/test/java/me/desair/tus/server/upload/disk/AbstractDiskBasedServiceTest.java diff --git a/src/main/java/me/desair/tus/server/upload/disk/AbstractDiskBasedService.java b/src/main/java/me/desair/tus/server/upload/disk/AbstractDiskBasedService.java index 6250332..5b0f1fd 100644 --- a/src/main/java/me/desair/tus/server/upload/disk/AbstractDiskBasedService.java +++ b/src/main/java/me/desair/tus/server/upload/disk/AbstractDiskBasedService.java @@ -37,7 +37,13 @@ protected Path getPathInStorageDirectory(UploadId id) { if (id == null) { return null; } else { - Path uploadPath = storagePath.resolve(id.toString()).normalize(); + Path uploadPath = + storagePath + .resolve( + id.getOriginalObject() != null + ? id.getOriginalObject().toString() + : id.toString()) + .normalize(); if (!uploadPath.startsWith(storagePath.normalize())) { throw new IllegalArgumentException( "The upload ID cannot point to a path outside the storage directory"); diff --git a/src/test/java/me/desair/tus/server/upload/disk/AbstractDiskBasedServiceTest.java b/src/test/java/me/desair/tus/server/upload/disk/AbstractDiskBasedServiceTest.java new file mode 100644 index 0000000..49a4667 --- /dev/null +++ b/src/test/java/me/desair/tus/server/upload/disk/AbstractDiskBasedServiceTest.java @@ -0,0 +1,49 @@ +package me.desair.tus.server.upload.disk; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import me.desair.tus.server.upload.UploadId; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class AbstractDiskBasedServiceTest { + + private Path storagePath; + private AbstractDiskBasedService service; + + @Before + public void setUp() throws IOException { + storagePath = Files.createTempDirectory("tus-test-storage"); + service = new AbstractDiskBasedService(storagePath.toString()) {}; + } + + @After + public void tearDown() throws IOException { + org.apache.commons.io.FileUtils.deleteDirectory(storagePath.toFile()); + } + + @Test + public void getPathInStorageDirectoryValidId() { + UploadId id = new UploadId("valid-id-123"); + Path resolved = service.getPathInStorageDirectory(id); + assertThat(resolved, is(storagePath.resolve("valid-id-123").normalize())); + } + + @Test + public void getPathInStorageDirectoryPathTraversal() { + try { + UploadId id = new UploadId("../../../../../etc/passwd"); + service.getPathInStorageDirectory(id); + fail("Expected IllegalArgumentException to be thrown for path traversal attempt"); + } catch (IllegalArgumentException e) { + assertThat( + e.getMessage(), is("The upload ID cannot point to a path outside the storage directory")); + } + } +}