Skip to content

Commit f5203dc

Browse files
committed
Merge branch '1-3-fix-GHSA-8xmh-xrvp-hwrf' into 1.3.x
2 parents f323821 + fb18fcc commit f5203dc

2 files changed

Lines changed: 238 additions & 5 deletions

File tree

src/Installing/WindowsInstall.php

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Php\Pie\Installing;
66

77
use Composer\IO\IOInterface;
8+
use FilesystemIterator;
89
use Php\Pie\Downloading\DownloadedPackage;
910
use Php\Pie\File\BinaryFile;
1011
use Php\Pie\File\WindowsDelete;
@@ -21,7 +22,11 @@
2122
use function file_exists;
2223
use function is_file;
2324
use function mkdir;
25+
use function realpath;
26+
use function sprintf;
27+
use function str_contains;
2428
use function str_replace;
29+
use function str_starts_with;
2530
use function strlen;
2631
use function substr;
2732

@@ -53,14 +58,19 @@ public function __invoke(
5358
$io->write('<info>Copied PDB to:</info> ' . $destinationPdbName);
5459
}
5560

56-
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($extractedSourcePath)) as $file) {
61+
$iterator = new RecursiveIteratorIterator(
62+
new RecursiveDirectoryIterator($extractedSourcePath, FilesystemIterator::SKIP_DOTS),
63+
);
64+
65+
foreach ($iterator as $file) {
5766
assert($file instanceof SplFileInfo);
5867

5968
/**
60-
* Skip directories, the main DLL, PDB
69+
* Skip directories, the main DLL, PDB and symlinks
6170
*/
6271
if (
6372
$file->isDir()
73+
|| $file->isLink()
6474
|| $this->normalisedPathsMatch($file->getPathname(), $sourceDllName)
6575
|| $this->normalisedPathsMatch($file->getPathname(), $sourcePdbName)
6676
) {
@@ -180,17 +190,42 @@ private function copyDependencyDll(TargetPlatform $targetPlatform, SplFileInfo $
180190
*/
181191
private function copyExtraFile(TargetPlatform $targetPlatform, DownloadedPackage $downloadedPackage, SplFileInfo $file): string
182192
{
183-
$destinationFullFilename = dirname($targetPlatform->phpBinaryPath->phpBinaryPath) . DIRECTORY_SEPARATOR
193+
$extrasRoot = dirname($targetPlatform->phpBinaryPath->phpBinaryPath) . DIRECTORY_SEPARATOR
184194
. 'extras' . DIRECTORY_SEPARATOR
185-
. $downloadedPackage->package->extensionName()->name() . DIRECTORY_SEPARATOR
186-
. substr($file->getPathname(), strlen($downloadedPackage->extractedSourcePath) + 1);
195+
. $downloadedPackage->package->extensionName()->name();
196+
197+
$relativeName = substr($file->getPathname(), strlen($downloadedPackage->extractedSourcePath) + 1);
198+
199+
if (str_contains($relativeName, '..' . DIRECTORY_SEPARATOR) || str_starts_with($relativeName, '..')) {
200+
throw new RuntimeException(sprintf(
201+
'Refusing to copy extra file with traversal segment: %s',
202+
$relativeName,
203+
));
204+
}
205+
206+
$destinationFullFilename = $extrasRoot . DIRECTORY_SEPARATOR . $relativeName;
187207

188208
$destinationPath = dirname($destinationFullFilename);
189209

190210
if (! file_exists($destinationPath)) {
191211
mkdir($destinationPath, 0777, true);
192212
}
193213

214+
$destinationReal = realpath($destinationPath);
215+
$extrasReal = realpath($extrasRoot);
216+
217+
if (
218+
$destinationReal === false
219+
|| $extrasReal === false
220+
|| ! str_starts_with($destinationReal . DIRECTORY_SEPARATOR, $extrasReal . DIRECTORY_SEPARATOR)
221+
) {
222+
throw new RuntimeException(sprintf(
223+
'Refusing to copy extra file: destination %s escapes extras root %s',
224+
$destinationPath,
225+
$extrasRoot,
226+
));
227+
}
228+
194229
if (! copy($file->getPathname(), $destinationFullFilename) || ! file_exists($destinationFullFilename) && ! is_file($destinationFullFilename)) {
195230
throw new RuntimeException('Failed to copy to ' . $destinationFullFilename);
196231
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Php\PieUnitTest\Installing;
6+
7+
use Composer\IO\BufferIO;
8+
use Composer\Package\CompletePackageInterface;
9+
use Php\Pie\DependencyResolver\Package;
10+
use Php\Pie\Downloading\DownloadedPackage;
11+
use Php\Pie\ExtensionName;
12+
use Php\Pie\ExtensionType;
13+
use Php\Pie\Installing\SetupIniFile;
14+
use Php\Pie\Installing\WindowsInstall;
15+
use Php\Pie\Platform\Architecture;
16+
use Php\Pie\Platform\OperatingSystem;
17+
use Php\Pie\Platform\OperatingSystemFamily;
18+
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
19+
use Php\Pie\Platform\TargetPlatform;
20+
use Php\Pie\Platform\ThreadSafetyMode;
21+
use Php\Pie\Platform\WindowsCompiler;
22+
use PHPUnit\Framework\Attributes\CoversClass;
23+
use PHPUnit\Framework\TestCase;
24+
use ReflectionClass;
25+
use RuntimeException;
26+
use SplFileInfo;
27+
28+
use function mkdir;
29+
use function symlink;
30+
use function sys_get_temp_dir;
31+
use function touch;
32+
use function uniqid;
33+
34+
#[CoversClass(WindowsInstall::class)]
35+
final class WindowsInstallTest extends TestCase
36+
{
37+
public function testCopyExtraFileWithTraversalThrowsException(): void
38+
{
39+
$tempDir = sys_get_temp_dir() . '/' . uniqid('pie_test_', true);
40+
mkdir($tempDir);
41+
mkdir($tempDir . '/source');
42+
43+
$setupIniFile = $this->createMock(SetupIniFile::class);
44+
$installer = new WindowsInstall($setupIniFile);
45+
46+
$package = new Package(
47+
$this->createMock(CompletePackageInterface::class),
48+
ExtensionType::PhpModule,
49+
ExtensionName::normaliseFromString('test'),
50+
'foo/bar',
51+
'1.2.3',
52+
null,
53+
);
54+
55+
$downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath($package, $tempDir . '/source');
56+
57+
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
58+
/** @phpstan-ignore property.notFound */
59+
(fn () => $this->phpBinaryPath = $tempDir . '/bin/php.exe')
60+
->bindTo($phpBinaryPath, PhpBinaryPath::class)();
61+
$phpBinaryPath->method('majorMinorVersion')->willReturn('8.5');
62+
$phpBinaryPath->method('extensionPath')->willReturn($tempDir . '/ext');
63+
mkdir($tempDir . '/ext', 0777, true);
64+
65+
$targetPlatform = new TargetPlatform(
66+
OperatingSystem::Windows,
67+
OperatingSystemFamily::Windows,
68+
$phpBinaryPath,
69+
Architecture::x86_64,
70+
ThreadSafetyMode::ThreadSafe,
71+
1,
72+
WindowsCompiler::VS16,
73+
);
74+
75+
mkdir($tempDir . '/bin', 0777, true);
76+
touch($tempDir . '/bin/php.exe');
77+
78+
$file = $this->createMock(SplFileInfo::class);
79+
$file->method('getPathname')->willReturn($tempDir . '/source/../escape.txt');
80+
81+
$reflection = new ReflectionClass(WindowsInstall::class);
82+
$method = $reflection->getMethod('copyExtraFile');
83+
$method->setAccessible(true);
84+
85+
$this->expectException(RuntimeException::class);
86+
$this->expectExceptionMessage('Refusing to copy extra file with traversal segment');
87+
88+
$method->invoke($installer, $targetPlatform, $downloadedPackage, $file);
89+
}
90+
91+
public function testCopyExtraFileWithDestinationEscapeThrowsException(): void
92+
{
93+
$tempDir = sys_get_temp_dir() . '/' . uniqid('pie_test_', true);
94+
mkdir($tempDir);
95+
mkdir($tempDir . '/source');
96+
mkdir($tempDir . '/bin', 0777, true);
97+
touch($tempDir . '/bin/php.exe');
98+
99+
$setupIniFile = $this->createMock(SetupIniFile::class);
100+
$installer = new WindowsInstall($setupIniFile);
101+
102+
$package = new Package(
103+
$this->createMock(CompletePackageInterface::class),
104+
ExtensionType::PhpModule,
105+
ExtensionName::normaliseFromString('test'),
106+
'foo/bar',
107+
'1.2.3',
108+
null,
109+
);
110+
111+
$downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath($package, $tempDir . '/source');
112+
113+
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
114+
/** @phpstan-ignore property.notFound */
115+
(fn () => $this->phpBinaryPath = $tempDir . '/bin/php.exe')
116+
->bindTo($phpBinaryPath, PhpBinaryPath::class)();
117+
$phpBinaryPath->method('majorMinorVersion')->willReturn('8.5');
118+
$phpBinaryPath->method('extensionPath')->willReturn($tempDir . '/ext');
119+
mkdir($tempDir . '/ext', 0777, true);
120+
121+
$targetPlatform = new TargetPlatform(
122+
OperatingSystem::Windows,
123+
OperatingSystemFamily::Windows,
124+
$phpBinaryPath,
125+
Architecture::x86_64,
126+
ThreadSafetyMode::ThreadSafe,
127+
1,
128+
WindowsCompiler::VS16,
129+
);
130+
131+
$extrasRoot = $tempDir . '/bin/extras/test';
132+
mkdir($extrasRoot, 0777, true);
133+
mkdir($tempDir . '/outside');
134+
symlink($tempDir . '/outside', $extrasRoot . '/escape');
135+
136+
$file = $this->createMock(SplFileInfo::class);
137+
$file->method('getPathname')->willReturn($tempDir . '/source/escape/file.txt');
138+
139+
$reflection = new ReflectionClass(WindowsInstall::class);
140+
$method = $reflection->getMethod('copyExtraFile');
141+
$method->setAccessible(true);
142+
143+
$this->expectException(RuntimeException::class);
144+
$this->expectExceptionMessage('escapes extras root');
145+
146+
$method->invoke($installer, $targetPlatform, $downloadedPackage, $file);
147+
}
148+
149+
public function testInvokeSkipsSymlinks(): void
150+
{
151+
$tempDir = sys_get_temp_dir() . '/' . uniqid('pie_test_', true);
152+
mkdir($tempDir);
153+
mkdir($tempDir . '/source');
154+
mkdir($tempDir . '/outside');
155+
touch($tempDir . '/outside/danger.txt');
156+
symlink($tempDir . '/outside/danger.txt', $tempDir . '/source/danger.link');
157+
158+
$setupIniFile = $this->createMock(SetupIniFile::class);
159+
$installer = new WindowsInstall($setupIniFile);
160+
161+
$package = new Package(
162+
$this->createMock(CompletePackageInterface::class),
163+
ExtensionType::PhpModule,
164+
ExtensionName::normaliseFromString('test'),
165+
'foo/bar',
166+
'1.2.3',
167+
null,
168+
);
169+
170+
$downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath($package, $tempDir . '/source');
171+
172+
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
173+
/** @phpstan-ignore property.notFound */
174+
(fn () => $this->phpBinaryPath = $tempDir . '/bin/php.exe')
175+
->bindTo($phpBinaryPath, PhpBinaryPath::class)();
176+
$phpBinaryPath->method('majorMinorVersion')->willReturn('8.5');
177+
$phpBinaryPath->method('extensionPath')->willReturn($tempDir . '/ext');
178+
mkdir($tempDir . '/ext', 0777, true);
179+
180+
$targetPlatform = new TargetPlatform(
181+
OperatingSystem::Windows,
182+
OperatingSystemFamily::Windows,
183+
$phpBinaryPath,
184+
Architecture::x86_64,
185+
ThreadSafetyMode::ThreadSafe,
186+
1,
187+
WindowsCompiler::VS16,
188+
);
189+
190+
touch($tempDir . '/source/php_test-1.2.3-8.5-ts-vs16-x86_64.dll');
191+
192+
$output = new BufferIO();
193+
194+
$installer->__invoke($downloadedPackage, $targetPlatform, $output, false);
195+
196+
self::assertStringNotContainsString('danger.link', $output->getOutput());
197+
}
198+
}

0 commit comments

Comments
 (0)