Skip to content

Commit 595538b

Browse files
committed
Merge branch '1.4.x' into 1.5.x for security vulnerability patches.
2 parents 9a20a16 + 5d1485c commit 595538b

30 files changed

Lines changed: 1020 additions & 276 deletions

.github/workflows/build-assets.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
php-versions:
2727
- '8.1'
2828
permissions:
29+
contents: read
2930
# id-token:write is required for build provenance attestation.
3031
id-token: write
3132
# attestations:write is required for build provenance attestation.
@@ -55,7 +56,7 @@ jobs:
5556
# unprivileged context, otherwise anyone could send a PR with malicious
5657
# code, which would store attestation that `php/pie` built the PHAR, and
5758
# it would look genuine. So this should NOT run for PR builds.
58-
if: github.event_name != 'pull_request'
59+
if: github.event_name != 'pull_request' && github.event.repository.visibility == 'public'
5960
uses: actions/attest@v4
6061
with:
6162
subject-path: '${{ github.workspace }}/pie.phar'
@@ -80,6 +81,7 @@ jobs:
8081
- macos-26
8182
# - windows-2025 - disabled for now, seems broken
8283
permissions:
84+
contents: read
8385
# id-token:write is required for build provenance attestation.
8486
id-token: write
8587
# attestations:write is required for build provenance attestation.
@@ -156,7 +158,7 @@ jobs:
156158
# unprivileged context, otherwise anyone could send a PR with malicious
157159
# code, which would store attestation that `php/pie` built the binaries,
158160
# and it would look genuine. So this should NOT run for PR builds.
159-
if: github.event_name != 'pull_request'
161+
if: github.event_name != 'pull_request' && github.event.repository.visibility == 'public'
160162
uses: actions/attest@v4
161163
with:
162164
subject-path: '${{ github.workspace }}/${{ env.PIE_BINARY_OUTPUT }}'

phpstan-baseline.neon

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ parameters:
3030
count: 1
3131
path: src/Command/RepositoryRemoveCommand.php
3232

33+
-
34+
message: '#^Cannot cast mixed to string\.$#'
35+
identifier: cast.string
36+
count: 1
37+
path: src/Command/SelfVerifyCommand.php
38+
3339
-
3440
message: '#^Cannot cast mixed to string\.$#'
3541
identifier: cast.string
@@ -329,3 +335,39 @@ parameters:
329335
identifier: property.notFound
330336
count: 4
331337
path: test/unit/SelfManage/BuildTools/PhpizeBuildToolFinderTest.php
338+
339+
-
340+
message: '#^Parameter \#1 \$csr of function Safe\\openssl_csr_sign expects OpenSSLCertificateSigningRequest\|string, bool\|OpenSSLCertificateSigningRequest given\.$#'
341+
identifier: argument.type
342+
count: 2
343+
path: test/unit/SelfManage/Verify/FallbackVerificationUsingOpenSslTest.php
344+
345+
-
346+
message: '#^Parameter \#3 \$private_key of function Safe\\openssl_csr_sign expects array\|OpenSSLAsymmetricKey\|OpenSSLCertificate\|string, OpenSSLAsymmetricKey\|null given\.$#'
347+
identifier: argument.type
348+
count: 2
349+
path: test/unit/SelfManage/Verify/FallbackVerificationUsingOpenSslTest.php
350+
351+
-
352+
message: '#^Parameter \#3 \$private_key of function Safe\\openssl_sign expects array\|OpenSSLAsymmetricKey\|OpenSSLCertificate\|string, OpenSSLAsymmetricKey\|null given\.$#'
353+
identifier: argument.type
354+
count: 1
355+
path: test/unit/SelfManage/Verify/FallbackVerificationUsingOpenSslTest.php
356+
357+
-
358+
message: '#^Parameter \#3 \$signature of method Php\\PieUnitTest\\SelfManage\\Verify\\FallbackVerificationUsingOpenSslTest\:\:mockAttestationResponse\(\) expects string, string\|null given\.$#'
359+
identifier: argument.type
360+
count: 1
361+
path: test/unit/SelfManage/Verify/FallbackVerificationUsingOpenSslTest.php
362+
363+
-
364+
message: '#^Parameter \#3 \$subject of function str_replace expects array\<string\>\|string, string\|null given\.$#'
365+
identifier: argument.type
366+
count: 1
367+
path: test/unit/SelfManage/Verify/FallbackVerificationUsingOpenSslTest.php
368+
369+
-
370+
message: '#^Parameter \#4 \$pemCertificate of method Php\\PieUnitTest\\SelfManage\\Verify\\FallbackVerificationUsingOpenSslTest\:\:mockAttestationResponse\(\) expects string, string\|null given\.$#'
371+
identifier: argument.type
372+
count: 1
373+
path: test/unit/SelfManage/Verify/FallbackVerificationUsingOpenSslTest.php

src/Command/SelfUpdateCommand.php

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use Composer\IO\IOInterface;
88
use Composer\IO\NullIO;
9-
use Composer\Util\HttpDownloader;
109
use Php\Pie\ComposerIntegration\PieComposerFactory;
1110
use Php\Pie\ComposerIntegration\PieComposerRequest;
1211
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
@@ -23,6 +22,7 @@
2322
use Php\Pie\Util\Emoji;
2423
use Php\Pie\Util\PieVersion;
2524
use Psr\Container\ContainerInterface;
25+
use Safe\Exceptions\FilesystemException;
2626
use Symfony\Component\Console\Attribute\AsCommand;
2727
use Symfony\Component\Console\Command\Command;
2828
use Symfony\Component\Console\Input\InputInterface;
@@ -116,9 +116,12 @@ public function execute(InputInterface $input, OutputInterface $output): int
116116
),
117117
);
118118

119-
$httpDownloader = new HttpDownloader($this->quieterConsoleIo, $composer->getConfig());
120-
$fetchLatestPieRelease = new FetchPieReleaseFromGitHub($this->githubApiBaseUrl, $httpDownloader);
121-
$verifyPiePhar = VerifyPieReleaseUsingAttestation::factory();
119+
$fetchLatestPieRelease = FetchPieReleaseFromGitHub::factory(
120+
$this->quieterConsoleIo,
121+
$composer->getConfig(),
122+
$this->githubApiBaseUrl,
123+
);
124+
$verifyPiePhar = VerifyPieReleaseUsingAttestation::factory($fetchLatestPieRelease);
122125

123126
if ($updateChannel === Channel::Nightly) {
124127
$latestRelease = new ReleaseMetadata(
@@ -177,12 +180,30 @@ public function execute(InputInterface $input, OutputInterface $output): int
177180
return Command::FAILURE;
178181
}
179182

183+
try {
184+
$pharContents = file_get_contents($pharFilename->filePath);
185+
} catch (FilesystemException) {
186+
$this->io->writeError(sprintf('<error>%s Failed to read the downloaded PHAR file %s</error>', Emoji::CROSS, $pharFilename->filePath));
187+
unlink($pharFilename->filePath);
188+
189+
return Command::FAILURE;
190+
}
191+
192+
try {
193+
$pharFilename->verifyContent($pharContents);
194+
} catch (Throwable) {
195+
$this->io->writeError(sprintf('<error>%s PHAR contents changed after verification; aborting self-update</error>', Emoji::CROSS));
196+
unlink($pharFilename->filePath);
197+
198+
return Command::FAILURE;
199+
}
200+
180201
$fullPathToSelf = ($this->fullPathToSelf)();
181202
$this->io->write(
182203
sprintf('Writing new version to %s', $fullPathToSelf),
183204
verbosity: IOInterface::VERBOSE,
184205
);
185-
SudoFilePut::contents($fullPathToSelf, file_get_contents($pharFilename->filePath));
206+
SudoFilePut::contents($fullPathToSelf, $pharContents);
186207
unlink($pharFilename->filePath);
187208

188209
$this->io->write(sprintf(

src/Command/SelfVerifyCommand.php

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@
55
namespace Php\Pie\Command;
66

77
use Composer\IO\IOInterface;
8+
use Composer\IO\NullIO;
9+
use Php\Pie\ComposerIntegration\PieComposerFactory;
10+
use Php\Pie\ComposerIntegration\PieComposerRequest;
11+
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
812
use Php\Pie\File\BinaryFile;
913
use Php\Pie\File\FullPathToSelf;
14+
use Php\Pie\SelfManage\Update\FetchPieReleaseFromGitHub;
1015
use Php\Pie\SelfManage\Update\ReleaseMetadata;
1116
use Php\Pie\SelfManage\Verify\FailedToVerifyRelease;
1217
use Php\Pie\SelfManage\Verify\VerifyPieReleaseUsingAttestation;
1318
use Php\Pie\Util\Emoji;
1419
use Php\Pie\Util\PieVersion;
20+
use Psr\Container\ContainerInterface;
1521
use Symfony\Component\Console\Attribute\AsCommand;
1622
use Symfony\Component\Console\Command\Command;
23+
use Symfony\Component\Console\Input\InputArgument;
1724
use Symfony\Component\Console\Input\InputInterface;
1825
use Symfony\Component\Console\Output\OutputInterface;
1926

@@ -25,9 +32,15 @@
2532
)]
2633
final class SelfVerifyCommand extends Command
2734
{
35+
private const ARGUMENT_VERSION = 'version';
36+
37+
/** @param non-empty-string $githubApiBaseUrl */
2838
public function __construct(
39+
private readonly string $githubApiBaseUrl,
2940
private readonly FullPathToSelf $fullPathToSelf,
3041
private readonly IOInterface $io,
42+
private readonly QuieterConsoleIO $quieterConsoleIo,
43+
private readonly ContainerInterface $container,
3144
) {
3245
parent::__construct();
3346
}
@@ -37,6 +50,11 @@ public function configure(): void
3750
parent::configure();
3851

3952
CommandHelper::configurePhpConfigOptions($this);
53+
$this->addArgument(
54+
self::ARGUMENT_VERSION,
55+
InputArgument::OPTIONAL,
56+
'The version of PIE you expect to be running (e.g. 1.4.4 or nightly)',
57+
);
4058
}
4159

4260
public function execute(InputInterface $input, OutputInterface $output): int
@@ -47,15 +65,40 @@ public function execute(InputInterface $input, OutputInterface $output): int
4765
return Command::FAILURE;
4866
}
4967

50-
$latestRelease = new ReleaseMetadata(PieVersion::get(), 'blah');
68+
$expectedVersion = (string) $input->getArgument(self::ARGUMENT_VERSION);
69+
70+
if ($expectedVersion === '') {
71+
$expectedVersion = PieVersion::get();
72+
$this->io->write(sprintf('<comment>No version specified, verifying against the version this PHAR claims to be (%s).</comment>', $expectedVersion));
73+
}
74+
75+
$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io);
76+
77+
CommandHelper::applyNoCacheOptionIfSet($input, $this->io);
78+
79+
$composer = PieComposerFactory::createPieComposer(
80+
$this->container,
81+
PieComposerRequest::noOperation(
82+
new NullIO(),
83+
$targetPlatform,
84+
),
85+
);
86+
87+
$fetchLatestPieRelease = FetchPieReleaseFromGitHub::factory(
88+
$this->quieterConsoleIo,
89+
$composer->getConfig(),
90+
$this->githubApiBaseUrl,
91+
);
92+
93+
$latestRelease = new ReleaseMetadata($expectedVersion, 'blah');
5194
$pharFilename = BinaryFile::fromFileWithSha256Checksum(($this->fullPathToSelf)());
52-
$verifyPiePhar = VerifyPieReleaseUsingAttestation::factory();
95+
$verifyPiePhar = VerifyPieReleaseUsingAttestation::factory($fetchLatestPieRelease);
5396

5497
try {
5598
$verifyPiePhar->verify($latestRelease, $pharFilename, $this->io);
5699
} catch (FailedToVerifyRelease $failedToVerifyRelease) {
57100
$this->io->writeError(sprintf(
58-
'<error>❌ Failed to verify the pie.phar release %s: %s</error>',
101+
'<error>❌ Failed to verify that this PIE binary is the authentic release %s: %s</error>',
59102
$latestRelease->tag,
60103
$failedToVerifyRelease->getMessage(),
61104
));
@@ -64,7 +107,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
64107
}
65108

66109
$this->io->write(sprintf(
67-
'<info>%s You are running an authentic PIE version %s.</info>',
110+
'<info>%s This is an authentic PIE release for version %s.</info>',
68111
Emoji::GREEN_CHECKMARK,
69112
$latestRelease->tag,
70113
));

src/ComposerIntegration/UninstallProcess.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ public function __invoke(
2828
PieComposerRequest $composerRequest,
2929
CompletePackageInterface $composerPackage,
3030
): void {
31-
$io = $composerRequest->pieOutput;
31+
$io = $composerRequest->pieOutput;
32+
$targetPlatform = $composerRequest->targetPlatform;
3233

3334
$piePackage = Package::fromComposerCompletePackage($composerPackage);
3435

35-
$affectedIniFiles = ($this->removeIniEntry)($piePackage, $composerRequest->targetPlatform, $io);
36+
$affectedIniFiles = ($this->removeIniEntry)($piePackage, $targetPlatform, $io);
3637

3738
if (count($affectedIniFiles) === 1) {
3839
$io->write(
@@ -52,6 +53,6 @@ public function __invoke(
5253
array_walk($affectedIniFiles, static fn (string $ini) => $io->write(' - ' . $ini));
5354
}
5455

55-
$io->write(sprintf('👋 <info>Removed extension:</info> %s', ($this->uninstall)($piePackage)->filePath));
56+
$io->write(sprintf('👋 <info>Removed extension:</info> %s', ($this->uninstall)($targetPlatform, $piePackage)->filePath));
5657
}
5758
}

src/DependencyResolver/Package.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use function Safe\parse_url;
3333
use function str_contains;
3434
use function str_starts_with;
35+
use function strlen;
3536
use function strtolower;
3637

3738
use const DIRECTORY_SEPARATOR;
@@ -88,7 +89,23 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com
8889

8990
$package->supportZts = $phpExtOptions['support-zts'] ?? true;
9091
$package->supportNts = $phpExtOptions['support-nts'] ?? true;
91-
$package->buildPath = $phpExtOptions['build-path'] ?? null;
92+
93+
$buildPath = $phpExtOptions['build-path'] ?? null;
94+
if ($buildPath !== null) {
95+
if (
96+
str_starts_with($buildPath, '/')
97+
|| str_starts_with($buildPath, '\\')
98+
|| (strlen($buildPath) > 1 && $buildPath[1] === ':')
99+
) {
100+
throw new InvalidArgumentException('php-ext.build-path must be a relative path.');
101+
}
102+
103+
if (str_contains($buildPath, '..')) {
104+
throw new InvalidArgumentException('php-ext.build-path cannot contain ".." segments.');
105+
}
106+
}
107+
108+
$package->buildPath = $buildPath;
92109

93110
$compatibleOsFamilies = $phpExtOptions['os-families'] ?? null;
94111
$incompatibleOsFamilies = $phpExtOptions['os-families-exclude'] ?? null;

src/Downloading/DownloadedPackage.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Php\Pie\DependencyResolver\Package;
88
use Php\Pie\Platform\PrePackagedSourceAssetName;
9+
use RuntimeException;
910
use Safe\Exceptions\FilesystemException;
1011

1112
use function array_map;
@@ -14,7 +15,9 @@
1415
use function is_dir;
1516
use function pathinfo;
1617
use function Safe\realpath;
18+
use function sprintf;
1719
use function str_replace;
20+
use function str_starts_with;
1821

1922
use const DIRECTORY_SEPARATOR;
2023
use const PATHINFO_FILENAME;
@@ -72,7 +75,7 @@ private static function overrideSourcePathUsingBuildPath(Package $package, strin
7275
}
7376

7477
try {
75-
$extractedSourcePathWithBuildPath = realpath(
78+
$candidate = realpath(
7679
$extractedSourcePath
7780
. DIRECTORY_SEPARATOR
7881
. str_replace('{version}', $package->version(), $package->buildPath()),
@@ -82,7 +85,17 @@ private static function overrideSourcePathUsingBuildPath(Package $package, strin
8285
return $extractedSourcePath;
8386
}
8487

85-
return $extractedSourcePathWithBuildPath;
88+
$extractedReal = realpath($extractedSourcePath);
89+
if (! str_starts_with($candidate . DIRECTORY_SEPARATOR, $extractedReal . DIRECTORY_SEPARATOR)) {
90+
throw new RuntimeException(sprintf(
91+
'php-ext.build-path %s resolved to %s, which is outside the extract directory %s',
92+
$package->buildPath(),
93+
$candidate,
94+
$extractedReal,
95+
));
96+
}
97+
98+
return $candidate;
8699
}
87100

88101
public static function fromPackageAndExtractedPath(Package $package, string $extractedSourcePath): self

src/File/BinaryFile.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Php\Pie\Util;
88

99
use function file_exists;
10+
use function hash;
1011
use function hash_equals;
1112
use function Safe\hash_file;
1213

@@ -44,7 +45,17 @@ public function verify(): void
4445
throw Util\FileNotFound::fromFilename($this->filePath);
4546
}
4647

47-
self::verifyAgainstOther(self::fromFileWithSha256Checksum($this->filePath));
48+
$this->verifyAgainstOther(self::fromFileWithSha256Checksum($this->filePath));
49+
}
50+
51+
/** @throws BinaryFileFailedVerification */
52+
public function verifyContent(string $content): void
53+
{
54+
$contentChecksum = hash(self::HASH_TYPE_SHA256, $content);
55+
56+
if (! hash_equals($this->checksum, $contentChecksum)) {
57+
throw BinaryFileFailedVerification::fromChecksumMismatch($this, new self($this->filePath, $contentChecksum));
58+
}
4859
}
4960

5061
/** @throws BinaryFileFailedVerification */

0 commit comments

Comments
 (0)