When the file cache is enabled, gcsfuse computes the local cache file path for an object by joining the cache directory with the bucket name and the GCS object name, with no sanitization or containment check. GCS object names may contain arbitrary characters including / and ... An object whose name contains enough ../ sequences resolves the cache path outside the configured cache directory, so caching that object's content writes attacker-controlled bytes to an arbitrary location on the host running gcsfuse (for example /etc/cron.d/... or /root/.ssh/authorized_keys). A user who mounts a shared or attacker-influenced bucket with the file cache enabled can have local files overwritten.
Details
Package: gcsfuse
Affected Versions: current main (v3 line)
internal/cache/util/util.go:
// GetObjectPath gives object path which is concatenation of bucket and object name.
func GetObjectPath(bucketName string, objectName string) string {
return path.Join(bucketName, objectName)
}
// GetDownloadPath gives file path to file in cache for given object path.
func GetDownloadPath(cacheDir string, objectPath string) string {
return path.Join(cacheDir, objectPath)
}
internal/cache/file/cache_handler.go builds the cache file path directly from these, using the raw object name, with no ../containment check:
fileSpec := data.FileSpec{
Path: util.GetDownloadPath(chr.cacheDir, util.GetObjectPath(bucketName, objectName)), // :106
...
}
return util.CreateFile(fileSpec, os.O_RDONLY) // creates the file (MkdirAll + OpenFile)
(The same construction is used at cache_handler.go:126 and :165.) objectName is the GCS object name, which is attacker-controlled for a shared/untrusted bucket. path.Join cleans the result but does not prevent leading .. from escaping: path.Join(cacheDir, path.Join(bucket, "../../../../../../etc/cron.d/pwn")) resolves to /etc/cron.d/pwn. There is no check that the result stays under cacheDir. When the object is read through the mount with the file cache enabled, its content is written to this escaped path.
By contrast, the newer shared-chunk cache hashes the object name before using it as a path (shared_chunk_cache_manager.go, computeObjectHash -> filepath.Join(cacheDir, prefix1, prefix2, hash)), which is safe; the legacy per-object file cache above is not.
When the file cache is enabled, gcsfuse computes the local cache file path for an object by joining the cache directory with the bucket name and the GCS object name, with no sanitization or containment check. GCS object names may contain arbitrary characters including
/and... An object whose name contains enough../sequences resolves the cache path outside the configured cache directory, so caching that object's content writes attacker-controlled bytes to an arbitrary location on the host running gcsfuse (for example/etc/cron.d/...or/root/.ssh/authorized_keys). A user who mounts a shared or attacker-influenced bucket with the file cache enabled can have local files overwritten.Details
Package: gcsfuse
Affected Versions: current main (v3 line)
internal/cache/util/util.go:internal/cache/file/cache_handler.gobuilds the cache file path directly from these, using the raw object name, with no../containment check:(The same construction is used at
cache_handler.go:126and:165.)objectNameis the GCS object name, which is attacker-controlled for a shared/untrusted bucket.path.Joincleans the result but does not prevent leading..from escaping:path.Join(cacheDir, path.Join(bucket, "../../../../../../etc/cron.d/pwn"))resolves to/etc/cron.d/pwn. There is no check that the result stays undercacheDir. When the object is read through the mount with the file cache enabled, its content is written to this escaped path.By contrast, the newer shared-chunk cache hashes the object name before using it as a path (
shared_chunk_cache_manager.go,computeObjectHash->filepath.Join(cacheDir, prefix1, prefix2, hash)), which is safe; the legacy per-object file cache above is not.