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 {