Skip to content

File cache writes to a path built from the unsanitized GCS object name, allowing an object name with ".." to escape the cache directory (arbitrary local file write) #4776

@geo-chen

Description

@geo-chen

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    p2P2questionCustomer Issue: question about how to use tool

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions