From 0ae3c152bd73fd6ce022d79372b0c475c0a30356 Mon Sep 17 00:00:00 2001 From: eugene yokota Date: Mon, 17 Aug 2026 14:09:03 -0400 Subject: [PATCH] [2.x] fix: Guard diskcache against path traversal (#9605) **Problem** There are several places in diskcache where resolve is called without guards. **Solution** This adds guards to prevent path traversal. --- .../scala/sbt/util/ActionCacheStore.scala | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala b/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala index 77cd12a91..eab0a4293 100644 --- a/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala +++ b/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala @@ -278,6 +278,15 @@ case class DiskActionCacheStore(base: Path, converter: FileConverter) try Files.exists(casFile) && Digest.sameDigest(casFile, digest) catch case _: NoSuchFileException => false + private def requireWithinBase(base: Path, target: Path, ref: HashedVirtualFileRef): Path = + val normalizedBase = base.toAbsolutePath.normalize() + val normalizedTarget = target.toAbsolutePath.normalize() + if normalizedTarget.startsWith(normalizedBase) then target + else + throw new IOException( + s"Refusing to sync ${ref.id}: resolved path $normalizedTarget is outside $normalizedBase" + ) + private def getBlobs(refs: Seq[HashedVirtualFileRef]): Seq[VirtualFile] = refs.flatMap: r => try @@ -343,9 +352,13 @@ case class DiskActionCacheStore(base: Path, converter: FileConverter) val result = linkOrCopy(outPath) afterFileWrite(ref, result, outputDirectory) result - val resolvedPath = converter.toPath(ref) match - case p if p.isAbsolute => p - case p => outputDirectory.resolve(p) + val resolvedPath = requireWithinBase( + outputDirectory, + converter.toPath(ref) match + case p if p.isAbsolute => p + case p => outputDirectory.resolve(p), + ref + ) resolvedPath match case p if !Files.exists(p) => // println(s"- syncFile: $p does not exist") @@ -407,7 +420,7 @@ case class DiskActionCacheStore(base: Path, converter: FileConverter) // manifest contains the list of files in the dirzip, and their hashes val m = ActionCache.manifestFromFile(mPath) m.outputFiles.foreach: ref => - val currentItem = converter.toPath(ref) + val currentItem = requireWithinBase(outputDirectory, converter.toPath(ref), ref) val shortPath = outputDirectory.relativize(currentItem).toString allPaths.remove(currentItem) val d = Digest(ref)