Skip to content

Commit bb07d8c

Browse files
authored
enhancement: prevent windows resolution outside target directory in Windows (#3326)
1 parent ee64625 commit bb07d8c

3 files changed

Lines changed: 62 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"type": "bugfix",
4+
"category": "S3",
5+
"description": "Aws\\S3\\Transfer::resolvesOutsideTargetDirectory() now treats both '/' and '\\' as directory separators when tokenizing the sink path. Aligns the legacy Transfer guard with the newer S3Transfer\\DirectoryDownloader on Windows, where PHP's filesystem layer resolves backslashes as separators."
6+
}
7+
]

src/S3/Transfer.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,13 @@ private function normalizePath($path)
244244

245245
private function resolvesOutsideTargetDirectory($sink, $objectKey)
246246
{
247+
// Split on both '/' and '\'. PHP on Windows treats '\' as a directory
248+
// separator in fopen/mkdir/dirname, so a key containing a raw
249+
// backslash byte can escape the destination if we only split on '/'.
250+
$separators = '#[/\\\\]#';
247251
$resolved = [];
248-
$sections = explode('/', $sink);
249-
$targetSectionsLength = count(explode('/', $objectKey));
252+
$sections = preg_split($separators, $sink);
253+
$targetSectionsLength = count(preg_split($separators, $objectKey));
250254
$targetSections = array_slice($sections, -($targetSectionsLength + 1));
251255
$targetDirectory = $targetSections[0];
252256

tests/S3/TransferTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,55 @@ public static function providedPathsOutsideTarget(): array
421421
];
422422
}
423423

424+
/**
425+
* Backslash-based traversal. On Windows, PHP's filesystem layer
426+
* (fopen, mkdir, dirname) treats '\' as a directory separator, so a
427+
* key containing a raw backslash escapes the destination even though
428+
* the historical guard only tokenized on '/'. The guard now splits on
429+
* both separators. Provider values are S3 keys that match the synced
430+
* 'bar/' prefix; after prefix strip the remaining objectKey contains
431+
* the backslash traversal.
432+
*/
433+
#[DataProvider('providedBackslashKeysOutsideTarget')]
434+
public function testCannotDownloadObjectsWithBackslashTraversal($key)
435+
{
436+
$this->expectException(\Aws\Exception\AwsException::class);
437+
$s3 = $this->getTestClient('s3');
438+
$lso = [
439+
'IsTruncated' => false,
440+
'Contents' => [
441+
['Key' => $key]
442+
]
443+
];
444+
$this->addMockResults($s3, [
445+
new Result($lso),
446+
new Result(['Body' => 'test']),
447+
]);
448+
449+
$dir = sys_get_temp_dir() . '/unittest';
450+
$this->deleteDirectory($dir);
451+
mkdir($dir);
452+
$res = fopen('php://temp', 'r+');
453+
$t = new Transfer($s3, 's3://foo/bar/', $dir, ['debug' => $res]);
454+
try {
455+
$t->transfer();
456+
} finally {
457+
$this->deleteDirectory($dir);
458+
}
459+
}
460+
461+
public static function providedBackslashKeysOutsideTarget(): array
462+
{
463+
return [
464+
// Direct analog of the AppSec-reported shape: `inbox/..\shell.php`
465+
// under a synced `inbox/` prefix. Here the prefix is `bar/`.
466+
'raw backslash traversal' => ['bar/..\\shell.php'],
467+
'backslash under nested subdir' => ['bar/inner\\..\\..\\shell.php'],
468+
'mixed forward-and-back separators' => ['bar/inner/..\\..\\shell.php'],
469+
'deep backslash climb' => ['bar/a\\..\\..\\b'],
470+
];
471+
}
472+
424473
public function testCanUploadToBareBucket()
425474
{
426475
$s3 = $this->getMockS3Client();

0 commit comments

Comments
 (0)