VYPR
Medium severityNVD Advisory· Published Feb 3, 2025· Updated Apr 15, 2026

CVE-2025-24961

CVE-2025-24961

Description

org.gaul S3Proxy implements the S3 API and proxies requests. Users of the filesystem and filesystem-nio2 storage backends could unintentionally expose local files to users. This issue has been addressed in version 2.6.0. Users are advised to upgrade. There are no known workarounds for this vulnerability.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

S3Proxy filesystem backends allow path traversal, exposing local files to unauthorized users; fixed in 2.6.0.

Vulnerability

Overview

CVE-2025-24961 is a path traversal vulnerability in the S3Proxy filesystem and filesystem-nio2 storage backends. The S3Proxy application implements the S3 API and proxies requests to various backends, including local filesystem storage [1]. The root cause is that the application did not properly validate or sanitize user-supplied bucket names and object keys before resolving them to filesystem paths, allowing an attacker to craft requests that traverse outside the intended base directory [2].

Exploitation

An attacker can exploit this vulnerability by sending specially crafted S3 API requests to the S3Proxy instance. For example, using path traversal sequences like ".." in bucket names or object keys. The attack is network-based and does not require authentication if the proxy is configured for anonymous access (as shown in the default configuration example with s3proxy.authorization=none) [1]. The attacker only needs network access to the S3Proxy endpoint, and the filesystem backend must be in use [2].

Impact

Successful exploitation allows an attacker to read, write, or delete arbitrary files on the host filesystem that the S3Proxy process has access to. This could lead to disclosure of sensitive data, modification of critical files, or denial of service. The impact is limited only by the permissions of the S3Proxy process, which typically runs with the privileges of the user starting it [2].

Mitigation

The vulnerability has been fixed in S3Proxy version 2.6.0. The commit addresses the issue by normalizing paths and adding a validation check (checkValidPath) in multiple operations including blob listing, reading, writing, copying, and deletion [4]. Users are strongly advised to upgrade to version 2.6.0 immediately. There are no known workarounds for this vulnerability [2]. If immediate upgrade is not possible, ensure that the S3Proxy is not exposed to untrusted networks and is configured with authentication enabled.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.gaul:s3proxyMaven
< 2.6.02.6.0

Patches

3
86b6ee4749aa

Prevent parent path traversal in filesystem-nio2

https://github.com/gaul/s3proxyAndrew GaulJan 28, 2025via ghsa
2 files changed · +91 6
  • src/main/java/org/gaul/s3proxy/nio2blob/AbstractNio2BlobStore.java+27 6 modified
    @@ -183,7 +183,10 @@ public final PageSet<? extends StorageMetadata> list(String container,
             } else {
                 prefix = "";
             }
    -        var pathPrefix = root.resolve(container).resolve(prefix);
    +        var containerPath = root.resolve(container);
    +        var pathPrefix = containerPath.resolve(prefix).normalize();
    +        checkValidPath(containerPath, pathPrefix);
    +        logger.debug("Listing blobs at: {}", pathPrefix);
             var set = ImmutableSortedSet.<StorageMetadata>naturalOrder();
             try {
                 listHelper(set, container, dirPrefix, pathPrefix, delimiter);
    @@ -344,7 +347,9 @@ public final Blob getBlob(String container, String key, GetOptions options) {
                 throw new ContainerNotFoundException(container, "");
             }
     
    -        var path = root.resolve(container).resolve(key);
    +        var containerPath = root.resolve(container);
    +        var path = containerPath.resolve(key);
    +        checkValidPath(containerPath, path);
             logger.debug("Getting blob at: {}", path);
     
             try {
    @@ -528,7 +533,9 @@ public final String putBlob(String container, Blob blob, PutOptions options) {
                 throw new ContainerNotFoundException(container, "");
             }
     
    -        var path = root.resolve(container).resolve(blob.getMetadata().getName());
    +        var containerPath = root.resolve(container);
    +        var path = containerPath.resolve(blob.getMetadata().getName());
    +        checkValidPath(containerPath, path);
             // TODO: should we use a known suffix to filter these out during list?
             var tmpPath = root.resolve(container).resolve(blob.getMetadata().getName() + "-" + UUID.randomUUID());
             logger.debug("Creating blob at: {}", path);
    @@ -693,7 +700,9 @@ public final String copyBlob(String fromContainer, String fromName,
         public final void removeBlob(String container, String key) {
             try {
                 var containerPath = root.resolve(container);
    -            var path = containerPath.resolve(key);
    +            var path = containerPath.resolve(key).normalize();
    +            checkValidPath(containerPath, path);
    +            logger.debug("Deleting blob at: {}", path);
                 Files.delete(path);
                 removeEmptyParentDirectories(containerPath, path.getParent());
             } catch (NoSuchFileException nsfe) {
    @@ -771,7 +780,10 @@ public final BlobAccess getBlobAccess(String container, String key) {
                 throw new KeyNotFoundException(container, key, "");
             }
     
    -        var path = root.resolve(container).resolve(key);
    +        var containerPath = root.resolve(container);
    +        var path = containerPath.resolve(key).normalize();
    +        checkValidPath(containerPath, path);
    +
             Set<PosixFilePermission> permissions;
             try {
                 permissions = Files.getPosixFilePermissions(path);
    @@ -791,7 +803,10 @@ public final void setBlobAccess(String container, String key, BlobAccess access)
                 throw new KeyNotFoundException(container, key, "");
             }
     
    -        var path = root.resolve(container).resolve(key);
    +        var containerPath = root.resolve(container);
    +        var path = containerPath.resolve(key).normalize();
    +        checkValidPath(containerPath, path);
    +
             Set<PosixFilePermission> permissions;
             try {
                 permissions = new HashSet<>(Files.getPosixFilePermissions(path));
    @@ -1117,4 +1132,10 @@ private static void writeCommonMetadataAttr(UserDefinedFileAttributeView view, B
                 writeStringAttributeIfPresent(view, XATTR_USER_METADATA_PREFIX + entry.getKey(), entry.getValue());
             }
         }
    +
    +    private static void checkValidPath(Path container, Path path) {
    +        if (!path.normalize().startsWith(container)) {
    +            throw new IllegalArgumentException("Invalid key name: path traversal attempt detected: " + path);
    +        }
    +    }
     }
    
  • src/test/java/org/gaul/s3proxy/AwsSdkTest.java+64 0 modified
    @@ -1717,6 +1717,70 @@ public Map.Entry<String, BlobStore> locateBlobStore(
             }
         }
     
    +    @Test
    +    public void testCopyRelativePath() throws Exception {
    +        assumeTrue(!blobStoreType.equals("azureblob-sdk"));
    +        try {
    +            client.copyObject(new CopyObjectRequest(
    +                    containerName, "../evil.txt", containerName, "good.txt"));
    +            Fail.failBecauseExceptionWasNotThrown(AmazonS3Exception.class);
    +        } catch (AmazonS3Exception e) {
    +            // expected
    +        }
    +    }
    +
    +    @Test
    +    public void testDeleteRelativePath() throws Exception {
    +        try {
    +            client.deleteObject(containerName, "../evil.txt");
    +            if (blobStoreType.equals("filesystem") || blobStoreType.equals("filesystem-nio2") || blobStoreType.equals("transient-nio2")) {
    +                Fail.failBecauseExceptionWasNotThrown(AmazonS3Exception.class);
    +            }
    +        } catch (AmazonS3Exception e) {
    +            // expected
    +        }
    +    }
    +
    +    @Test
    +    public void testGetRelativePath() throws Exception {
    +        try {
    +            client.getObject(containerName, "../evil.txt");
    +            Fail.failBecauseExceptionWasNotThrown(AmazonS3Exception.class);
    +        } catch (AmazonS3Exception e) {
    +            // expected
    +        }
    +    }
    +
    +    @Test
    +    public void testPutRelativePath() throws Exception {
    +        try {
    +            var metadata = new ObjectMetadata();
    +            metadata.setContentLength(BYTE_SOURCE.size());
    +            PutObjectResult result = client.putObject(containerName, "../evil.txt",
    +                    BYTE_SOURCE.openStream(), metadata);
    +            if (blobStoreType.equals("filesystem") || blobStoreType.equals("filesystem-nio2") || blobStoreType.equals("transient-nio2")) {
    +                Fail.failBecauseExceptionWasNotThrown(AmazonS3Exception.class);
    +            }
    +        } catch (AmazonS3Exception e) {
    +            // expected
    +        }
    +    }
    +
    +    @Test
    +    public void testListRelativePath() throws Exception {
    +        assumeTrue(!blobStoreType.equals("filesystem"));
    +        try {
    +            client.listObjects(new ListObjectsRequest()
    +                    .withBucketName(containerName)
    +                    .withPrefix("../evil/"));
    +            if (blobStoreType.equals("filesystem") || blobStoreType.equals("filesystem-nio2") || blobStoreType.equals("transient-nio2")) {
    +                Fail.failBecauseExceptionWasNotThrown(AmazonS3Exception.class);
    +            }
    +        } catch (AmazonS3Exception e) {
    +            // expected
    +        }
    +    }
    +
         private static final class NullX509TrustManager
                 implements X509TrustManager {
             @Override
    
b0819e0ef5e0

more validation for containerName and blobKey to avoid access escape

https://github.com/apache/jcloudsjixinchiApr 3, 2024via ghsa
3 files changed · +13 0
  • apis/filesystem/src/main/java/org/jclouds/filesystem/predicates/validators/internal/FilesystemBlobKeyValidatorImpl.java+2 0 modified
    @@ -38,6 +38,8 @@ public void validate(String name) throws IllegalArgumentException {
             //blobkey cannot start with / (or \ in Windows) character
             if (name.startsWith("\\") || name.startsWith("/"))
                 throw new IllegalArgumentException("Blob key '" + name + "' cannot start with \\ or /");
    +        if (name.contains("../"))
    +            throw new IllegalArgumentException("Blob key '" + name + "' cannot contains ../");
         }
     
     }
    
  • apis/filesystem/src/main/java/org/jclouds/filesystem/predicates/validators/internal/FilesystemContainerNameValidatorImpl.java+2 0 modified
    @@ -38,6 +38,8 @@ public void validate(String name) throws IllegalArgumentException {
             //container name cannot contains / (or \ in Windows) character
             if (name.contains("\\") || name.contains("/"))
                 throw new IllegalArgumentException("Container name '" + name + "' cannot contain \\ or /");
    +        if (name.equals(".") || name.equals(".."))
    +            throw new IllegalArgumentException("Container name cannot be . or ..");
         }
     
     }
    
  • apis/filesystem/src/main/java/org/jclouds/filesystem/strategy/internal/FilesystemStorageStrategyImpl.java+9 0 modified
    @@ -187,6 +187,7 @@ public boolean createContainerInLocation(String container, Location location, Cr
     
        @Override
        public ContainerAccess getContainerAccess(String container) {
    +      filesystemContainerNameValidator.validate(container);
           File file = new File(buildPathStartingFromBaseDir(container));
           if (!file.exists()) {
              throw new ContainerNotFoundException(container, "in getContainerAccess");
    @@ -217,6 +218,7 @@ public ContainerAccess getContainerAccess(String container) {
     
        @Override
        public void setContainerAccess(String container, ContainerAccess access) {
    +      filesystemContainerNameValidator.validate(container);
           Path path = new File(buildPathStartingFromBaseDir(container)).toPath();
     
           if ( isWindows() ) {
    @@ -310,6 +312,7 @@ else if (object.isDirectory() && (optsPrefix.endsWith(File.separator) || isNullO
     
        @Override
        public StorageMetadata getContainerMetadata(String container) {
    +      filesystemContainerNameValidator.validate(container);
           MutableStorageMetadata metadata = new MutableStorageMetadataImpl();
           metadata.setName(container);
           metadata.setType(StorageType.CONTAINER);
    @@ -378,6 +381,8 @@ public String apply(String string) {
     
        @Override
        public Blob getBlob(final String container, final String key) {
    +      filesystemContainerNameValidator.validate(container);
    +      filesystemBlobKeyValidator.validate(key);
           BlobBuilder builder = blobBuilders.get();
           builder.name(key);
           File file = getFileForBlobKey(container, key);
    @@ -658,6 +663,8 @@ public void removeBlob(final String container, final String blobKey) {
     
        @Override
        public BlobAccess getBlobAccess(String containerName, String blobName) {
    +      filesystemContainerNameValidator.validate(containerName);
    +      filesystemBlobKeyValidator.validate(blobName);
           if (!new File(buildPathStartingFromBaseDir(containerName)).exists()) {
              throw new ContainerNotFoundException(containerName, "in getBlobAccess");
           }
    @@ -691,6 +698,8 @@ public BlobAccess getBlobAccess(String containerName, String blobName) {
     
        @Override
        public void setBlobAccess(String container, String name, BlobAccess access) {
    +      filesystemContainerNameValidator.validate(container);
    +      filesystemBlobKeyValidator.validate(name);
           Path path = new File(buildPathStartingFromBaseDir(container, name)).toPath();
           if ( isWindows() ) {
              try {
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.