CVE-2023-51651: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: AWS SDK for PHP is the Amazon Web Services software development kit for PHP. Within the scope of requests to S3 object keys and/or prefixes containing a Unix double-dot, a URI path traversal is possible. The issue exists in the `buildEndpoint` method in the RestSerializer component of the AWS SDK for PHP v3 prior to 3.288.1. The `buildEndpoint` method relies on the Guzzle Psr7 UriResolver utility, which strips dot segments from the request path in accordance with RFC 3986. Under certain conditions, this could lead to an arbitrary object being accessed. This issue has been patched in version 3.288.1.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 3.3
Published: 2023-12-22T21:15Z
Repository: https://github.com/aws/aws-sdk-php
Language: PHP
Files changed: .changes/nextrelease/s3-dot-segment.json, src/Api/Serializer/RestSerializer.php, tests/S3/S3ClientTest.php
Commit date: 2023-11-22 14:30:39 -0500
Commit message: bugfix: dot segment handling (#2835)


Diff: diff --git a/.changes/nextrelease/s3-dot-segment.json b/.changes/nextrelease/s3-dot-segment.json
new file mode 100644
index 000000000..afe6799b5
--- /dev/null
+++ b/.changes/nextrelease/s3-dot-segment.json
@@ -0,0 +1,7 @@
+[
+  {
+    "type": "bugfix",
+    "category": "s3",
+    "description": "Disables transformation of request URI paths with dot segments"
+  }
+]
diff --git a/src/Api/Serializer/RestSerializer.php b/src/Api/Serializer/RestSerializer.php
index dbd925ad1..db431e5e7 100644
--- a/src/Api/Serializer/RestSerializer.php
+++ b/src/Api/Serializer/RestSerializer.php
@@ -240,18 +240,25 @@ abstract class RestSerializer
         ) {
             if (substr($path, -1) === '/' && $relative[0] === '/') {
                 $path = rtrim($path, '/');
             }
             $relative = $path . $relative;
+
+            if (strpos($relative, '../') !== false) {
+                if ($relative[0] !== '/') {
+                    $relative = '/' . $relative;
+                }
+                return new Uri($this->endpoint . $relative);
+            }
         }
         // If endpoint has path, remove leading '/' to preserve URI resolution.
         if ($path && $relative[0] === '/') {
             $relative = substr($relative, 1);
         }
 
-        //Append path to endpoint when leading '//...' present
-        // as uri cannot be properly resolved
+        //Append path to endpoint when leading '//...'
+        // present as uri cannot be properly resolved
         if ($this->api->isModifiedModel()
             && strpos($relative, '//') === 0
         ) {
             return new Uri($this->endpoint . $relative);
         }
diff --git a/tests/S3/S3ClientTest.php b/tests/S3/S3ClientTest.php
index 9e6eabf8b..4bd1f3574 100644
--- a/tests/S3/S3ClientTest.php
+++ b/tests/S3/S3ClientTest.php
@@ -2319,6 +2319,59 @@ EOXML;
                 );
             })
         );
         $s3->execute($command);
     }
+
+    /**
+     * @dataProvider dotSegmentProvider
+     */
+    public function testHandlesDotSegmentsInKey($key, $expectedUri)
+    {
+        $s3 = $this->getTestClient('s3');
+        $this->addMockResults($s3, [[]]);
+        $command = $s3->getCommand('getObject', ['Bucket' => 'foo', 'Key' => $key]);
+        $command->getHandlerList()->appendSign(
+            Middleware::tap(function ($cmd, $req) use ($expectedUri) {
+                $this->assertSame($expectedUri, (string) $req->getUri());
+            })
+        );
+        $s3->execute($command);
+    }
+
+    public function dotSegmentProvider()
+    {
+        return [
+            ['../foo' , 'https://foo.s3.amazonaws.com/../foo'],
+            ['bar/../../foo', 'https://foo.s3.amazonaws.com/bar/../../foo'],
+            ['/../foo', 'https://foo.s3.amazonaws.com//../foo'],
+            ['foo/bar/../baz', 'https://foo.s3.amazonaws.com/foo/bar/../baz']
+        ];
+    }
+
+    /**
+     * @dataProvider dotSegmentPathStyleProvider
+     */
+    public function testHandlesDotSegmentsInKeyWithPathStyle($key, $expectedUri)
+    {
+        $s3 = $this->getTestClient('s3', ['use_path_style_endpoint' => true]);
+        $this->addMockResults($s3, [[]]);
+        $command = $s3->getCommand('getObject', ['Bucket' => 'foo', 'Key' => $key]);
+        $command->getHandlerList()->appendSign(
+            Middleware::tap(function ($cmd, $req) use ($expectedUri) {
+                $this->assertSame($expectedUri, (string) $req->getUri());
+            })
+        );
+        $s3->execute($command);
+    }
+
+    public function dotSegmentPathStyleProvider()
+    {
+        return [
+            ['../foo' , 'https://s3.amazonaws.com/foo/foo/../foo'],
+            ['bar/../../foo', 'https://s3.amazonaws.com/foo/foo/bar/../../foo'],
+            ['/../foo', 'https://s3.amazonaws.com/foo/foo//../foo'],
+            ['foo/bar/../baz', 'https://s3.amazonaws.com/foo/foo/foo/bar/../baz'],
+        ];
+    }
+
 }

Vulnerable code:         //Append path to endpoint when leading '//...' present
        // as uri cannot be properly resolved
Fixed code: [
  {
    "type": "bugfix",
    "category": "s3",
    "description": "Disables transformation of request URI paths with dot segments"
  }
]

            if (strpos($relative, '../') !== false) {
                if ($relative[0] !== '/') {
                    $relative = '/' . $relative;
                }
                return new Uri($this->endpoint . $relative);
            }
        //Append path to endpoint when leading '//...'
        // present as uri cannot be properly resolved

    /**
     * @dataProvider dotSegmentProvider
     */
    public function testHandlesDotSegmentsInKey($key, $expectedUri)
    {
        $s3 = $this->getTestClient('s3');
        $this->addMockResults($s3, [[]]);
        $command = $s3->getCommand('getObject', ['Bucket' => 'foo', 'Key' => $key]);
        $command->getHandlerList()->appendSign(
            Middleware::tap(function ($cmd, $req) use ($expectedUri) {
                $this->assertSame($expectedUri, (string) $req->getUri());
            })
        );
        $s3->execute($command);
    }

    public function dotSegmentProvider()
    {
        return [
            ['../foo' , 'https://foo.s3.amazonaws.com/../foo'],
            ['bar/../../foo', 'https://foo.s3.amazonaws.com/bar/../../foo'],
            ['/../foo', 'https://foo.s3.amazonaws.com//../foo'],
            ['foo/bar/../baz', 'https://foo.s3.amazonaws.com/foo/bar/../baz']
        ];
    }

    /**
     * @dataProvider dotSegmentPathStyleProvider
     */
    public function testHandlesDotSegmentsInKeyWithPathStyle($key, $expectedUri)
    {
        $s3 = $this->getTestClient('s3', ['use_path_style_endpoint' => true]);
        $this->addMockResults($s3, [[]]);
        $command = $s3->getCommand('getObject', ['Bucket' => 'foo', 'Key' => $key]);
        $command->getHandlerList()->appendSign(
            Middleware::tap(function ($cmd, $req) use ($expectedUri) {
                $this->assertSame($expectedUri, (string) $req->getUri());
            })
        );
        $s3->execute($command);
    }

    public function dotSegmentPathStyleProvider()
    {
        return [
            ['../foo' , 'https://s3.amazonaws.com/foo/foo/../foo'],
            ['bar/../../foo', 'https://s3.amazonaws.com/foo/foo/bar/../../foo'],
            ['/../foo', 'https://s3.amazonaws.com/foo/foo//../foo'],
            ['foo/bar/../baz', 'https://s3.amazonaws.com/foo/foo/foo/bar/../baz'],
        ];
    }