diff --git a/src/Building/PlaceholderReplacer.php b/src/Building/PlaceholderReplacer.php index 3383aa61..95fa171f 100644 --- a/src/Building/PlaceholderReplacer.php +++ b/src/Building/PlaceholderReplacer.php @@ -57,6 +57,13 @@ public function replacePlaceholdersWithPlaceholderReplacements(IOInterface $io, foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($downloadedPackage->extractedSourcePath)) as $file) { assert($file instanceof SplFileInfo); + // Refuse to follow symlinks into files the package author did not + // legitimately own at build time (e.g., an archive entry that points + // at the invoking user's $HOME). + if ($file->isLink()) { + continue; + } + if (! $file->isFile() || ! in_array($file->getExtension(), self::FILE_EXTENSIONS)) { continue; } diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index 105c0052..e0f41347 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -129,6 +129,15 @@ function (OperationInterface $operation): void { $this->composerRequest->pieOutput->write('Found prebuilt archive: ' . $url); $composerPackage->setDistUrl($url); + // Composer's dist-sha was computed against the original + // Packagist URL; once we swap to a release-asset URL the + // FileDownloader has nothing to validate the new bytes + // against. Surface that so the caller knows HTTPS-to-origin + // is the only integrity guarantee left. + $this->composerRequest->pieOutput->write( + 'Note: dist-sha integrity check is not available for prebuilt-binary URLs; HTTPS to the release-asset origin is the only integrity guarantee.', + ); + if (pathinfo($url, PATHINFO_EXTENSION) === 'tgz') { $composerPackage->setDistType('tar'); } diff --git a/src/ConfigureOption.php b/src/ConfigureOption.php index 12657a59..f4dca0ad 100644 --- a/src/ConfigureOption.php +++ b/src/ConfigureOption.php @@ -28,6 +28,14 @@ public static function fromComposerJsonDefinition(array $configureOptionDefiniti { Assert::keyExists($configureOptionDefinition, 'name'); Assert::stringNotEmpty($configureOptionDefinition['name']); + // Restrict to identifier characters that match real ./configure flag + // conventions. Whitespace and shell metacharacters in this field flow + // verbatim into argv, log output, and installed.json metadata. + Assert::regex( + $configureOptionDefinition['name'], + '/^[a-zA-Z][a-zA-Z0-9_-]*$/', + 'php-ext.configure-options[].name must be a configure-flag identifier (got %s)', + ); $needsValue = false; if (array_key_exists('needs-value', $configureOptionDefinition)) { diff --git a/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php index 96abe3e6..15414ee0 100644 --- a/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php +++ b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php @@ -19,6 +19,7 @@ use function file_exists; use function in_array; use function is_dir; +use function preg_quote; use function Safe\file_get_contents; use function Safe\preg_replace; use function Safe\scandir; @@ -67,10 +68,14 @@ static function (string $path) use ($additionalIniDirectory): bool { // Make sure all symlinks are resolved $allIniFiles = array_filter(array_map('realpath', $allIniFiles)); + // Anchor on the right with \b so uninstalling `foo` doesn't also + // rewrite the prefix of `extension=foo_other`. preg_quote on the + // extension name is defence-in-depth in case future ExtensionName + // validation ever loosens past `^[A-Za-z][a-zA-Z0-9_]+$`. $regex = sprintf( - '/^(%s\s*=\s*%s)/m', + '/^(%s\s*=\s*%s)\b/m', $package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension', - $package->extensionName()->name(), + preg_quote($package->extensionName()->name(), '/'), ); $updatedIniFiles = []; diff --git a/src/Installing/WindowsInstall.php b/src/Installing/WindowsInstall.php index 21a7f5e4..ae5e38ec 100644 --- a/src/Installing/WindowsInstall.php +++ b/src/Installing/WindowsInstall.php @@ -58,10 +58,13 @@ public function __invoke( assert($file instanceof SplFileInfo); /** - * Skip directories, the main DLL, PDB + * Skip directories, the main DLL, PDB, and any symlinks the archive + * may have shipped (symlink-followed targets fall outside the source + * dir's containment guarantees). */ if ( $file->isDir() + || $file->isLink() || $this->normalisedPathsMatch($file->getPathname(), $sourceDllName) || $this->normalisedPathsMatch($file->getPathname(), $sourcePdbName) ) { diff --git a/src/SelfManage/Verify/FallbackVerificationUsingOpenSsl.php b/src/SelfManage/Verify/FallbackVerificationUsingOpenSsl.php index 3c2a80ae..e62e3cdf 100644 --- a/src/SelfManage/Verify/FallbackVerificationUsingOpenSsl.php +++ b/src/SelfManage/Verify/FallbackVerificationUsingOpenSsl.php @@ -36,9 +36,12 @@ public function __construct( public function verify(ReleaseMetadata $releaseMetadata, BinaryFile $pharFilename, IOInterface $io): void { - $io->write( - 'Falling back to basic verification. To use full verification, install the `gh` CLI tool.', - verbosity: IOInterface::VERBOSE, + // The fallback verifier checks cert chain, cert extension claims, DSSE + // subject digest, and DSSE signature, but does NOT validate Rekor + // transparency-log inclusion. `gh attestation verify` does. Surface the + // reduced guarantees so users on shared / air-gapped hosts know. + $io->writeError( + 'Falling back to OpenSSL verification (no Rekor inclusion check). Install `gh` for full attestation verification.', ); try {