diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index e69b65b21..b2f68952a 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -133,7 +133,8 @@ EOT throw new \RuntimeException('--file and --global can not be combined'); } - $this->config = Factory::createConfig($this->getIO()); + $io = $this->getIO(); + $this->config = Factory::createConfig($io); // Get the local composer.json, global config.json, or if the user // passed in a file to use @@ -146,14 +147,14 @@ EOT file_put_contents($configFile, "{\n}\n"); } - $this->configFile = new JsonFile($configFile); + $this->configFile = new JsonFile($configFile, null, $io); $this->configSource = new JsonConfigSource($this->configFile); $authConfigFile = $input->getOption('global') ? ($this->config->get('home') . '/auth.json') : dirname(realpath($configFile)) . '/auth.json'; - $this->authConfigFile = new JsonFile($authConfigFile); + $this->authConfigFile = new JsonFile($authConfigFile, null, $io); $this->authConfigSource = new JsonConfigSource($this->authConfigFile, true); // initialize the global file if it's not there diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 01930e008..e17378c78 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -44,23 +44,25 @@ class RemoteFilesystem /** * Constructor. * - * @param IOInterface $io The IO instance - * @param Config $config The config - * @param array $options The options + * @param IOInterface $io The IO instance + * @param Config $config The config + * @param array $options The options + * @param bool $disableTls */ - public function __construct(IOInterface $io, Config $config = null, $options = array(), $disableTls = false) + public function __construct(IOInterface $io, Config $config = null, array $options = array(), $disableTls = false) { $this->io = $io; - /** - * Setup TLS options - * The cafile option can be set via config.json - */ + // Setup TLS options + // The cafile option can be set via config.json if ($disableTls === false) { $this->options = $this->getTlsDefaults(); if (isset($options['ssl']['cafile']) - && (!is_readable($options['ssl']['cafile']) - || !\openssl_x509_parse(file_get_contents($options['ssl']['cafile'])))) { + && ( + !is_readable($options['ssl']['cafile']) + || !self::validateCaFile(file_get_contents($options['ssl']['cafile'])) + ) + ) { throw new TransportException('The configured cafile was not valid or could not be read.'); } } else { @@ -316,15 +318,17 @@ class RemoteFilesystem } // Check if the failure was due to a Common Name mismatch with remote SSL cert and retry once (excl normal retry) - if (false === $result) { - if ($this->retryTls === true - && preg_match("|did not match expected CN|i", $errorMessage) - && preg_match("|Peer certificate CN=`(.*)' did not match|i", $errorMessage, $matches)) { - $this->retryTls = false; - $expectedCommonName = $matches[1]; - $this->io->write(" Retrying download from ".$originUrl." with SSL Cert Common Name (CN): ".$expectedCommonName.""); - return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress, $expectedCommonName); - } + if ( + false === $result + && $this->retryTls === true + && preg_match('{did not match expected CN}i', $errorMessage) + && preg_match("{Peer certificate CN=`(.*)' did not match}i", $errorMessage, $matches) + ) { + $this->retryTls = false; + $expectedCommonName = $matches[1]; + $this->io->write(" Retrying download from ".$originUrl." with SSL Cert Common Name (CN): ".$expectedCommonName.""); + + return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress, $expectedCommonName); } if ($this->retry) { @@ -485,33 +489,30 @@ class RemoteFilesystem protected function getOptionsForUrl($originUrl, $additionalOptions, $validCommonName = '') { + $tlsOptions = array(); + // Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN if ($this->disableTls === false) { - if (!preg_match("|^https?://|", $this->fileUrl)) { + if (!preg_match('{^https?://}', $this->fileUrl)) { $host = $originUrl; } else { $host = parse_url($this->fileUrl, PHP_URL_HOST); } - /** - * This is sheer painful, but hopefully it'll be a footnote once SAN support - * reaches PHP 5.4 and 5.5... - * Side-effect: We're betting on the CN being either a wildcard or www, e.g. *.github.com or www.example.com. - * TODO: Consider something more explicitly user based. - */ + + // This is sheer painful, but hopefully it'll be a footnote once SAN support + // reaches PHP 5.4 and 5.5... + // Side-effect: We're betting on the CN being either a wildcard or www, e.g. *.github.com or www.example.com. if (strlen($validCommonName) > 0) { - if (!preg_match("|".$host."$|i", $validCommonName) - || (count(explode('.', $validCommonName)) - count(explode('.', $host))) > 1) { + if ( + !preg_match('{'.$host.'$}i', $validCommonName) + || (count(explode('.', $validCommonName)) - count(explode('.', $host))) > 1 + ) { throw new TransportException('Unable to read or match the Common Name (CN) from the remote SSL certificate.'); } $host = $validCommonName; } - $this->options['ssl']['CN_match'] = $host; - $this->options['ssl']['SNI_server_name'] = $host; - } - if (defined('HHVM_VERSION')) { - $phpVersion = 'HHVM ' . HHVM_VERSION; - } else { - $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; + $tlsOptions['ssl']['CN_match'] = $host; + $tlsOptions['ssl']['SNI_server_name'] = $host; } $headers = array(); @@ -520,7 +521,7 @@ class RemoteFilesystem $headers[] = 'Accept-Encoding: gzip'; } - $options = array_replace_recursive($this->options, $additionalOptions); + $options = array_replace_recursive($this->options, $tlsOptions, $additionalOptions); if (!$this->degradedMode) { // degraded mode disables HTTP/1.1 which causes issues with some bad // proxies/software due to the use of chunked encoding @@ -613,19 +614,27 @@ class RemoteFilesystem * The user may go download one if this occurs. */ if (!isset($this->options['ssl']['cafile'])) { - $result = $this->getSystemCaRootBundlePath(); + $result = self::getSystemCaRootBundlePath(); if ($result) { - if (preg_match("|^phar://|", $result)) { - $tmp = rtrim(sys_get_temp_dir(), '\\/'); - $target = $tmp . DIRECTORY_SEPARATOR . 'composer-cacert.pem'; - $cacert = file_get_contents($result); - $write = file_put_contents($target, $cacert, LOCK_EX); - if (!$write) { - throw new TransportException('Unable to write bundled cacert.pem to: '.$target); - } - $options['ssl']['cafile'] = $target; + if (preg_match('{^phar://}', $result)) { + $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert.pem'; + + // use stream_copy_to_stream instead of copy + // to work around https://bugs.php.net/bug.php?id=64634 + $source = fopen($result, 'r'); + $target = fopen($targetPath, 'w+'); + stream_copy_to_stream($source, $target); + fclose($source); + fclose($target); + unset($source, $target); + + $options['ssl']['cafile'] = $targetPath; } else { - $options['ssl']['cafile'] = $result; + if (is_dir($result)) { + $options['ssl']['capath'] = $result; + } elseif ($result) { + $options['ssl']['cafile'] = $result; + } } } else { throw new TransportException('A valid cafile could not be located automatically.'); @@ -674,15 +683,17 @@ class RemoteFilesystem * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - protected static function getSystemCaRootBundlePath() + private static function getSystemCaRootBundlePath() { - if (isset($found)) { + static $found = null; + if ($found !== null) { return $found; } + // If SSL_CERT_FILE env variable points to a valid certificate/bundle, use that. // This mimics how OpenSSL uses the SSL_CERT_FILE env variable. $envCertFile = getenv('SSL_CERT_FILE'); - if ($envCertFile && is_readable($envCertFile) && \openssl_x509_parse(file_get_contents($envCertFile))) { + if ($envCertFile && is_readable($envCertFile) && self::validateCaFile(file_get_contents($envCertFile))) { // Possibly throw exception instead of ignoring SSL_CERT_FILE if it's invalid? return $envCertFile; } @@ -696,25 +707,50 @@ class RemoteFilesystem '/opt/local/share/curl/curl-ca-bundle.crt', // OS X macports, curl-ca-bundle package '/usr/local/share/curl/curl-ca-bundle.crt', // Default cURL CA bunde path (without --with-ca-bundle option) '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat? + '/etc/ssl/cert.pem', // OpenBSD + '/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x __DIR__.'/../../../res/cacert.pem', // Bundled with Composer ); - static $found = false; $configured = ini_get('openssl.cafile'); - if ($configured && strlen($configured) > 0 && is_readable($caBundle) && \openssl_x509_parse(file_get_contents($caBundle))) { + if ($configured && strlen($configured) > 0 && is_readable($caBundle) && self::validateCaFile(file_get_contents($caBundle))) { $found = true; $caBundle = $configured; } else { foreach ($caBundlePaths as $caBundle) { - if (is_readable($caBundle) && \openssl_x509_parse(file_get_contents($caBundle))) { + if (@is_readable($caBundle) && self::validateCaFile(file_get_contents($caBundle))) { $found = true; break; } } + if (!$found) { + foreach ($caBundlePaths as $caBundle) { + $caBundle = dirname($caBundle); + if (is_dir($caBundle) && glob($caBundle.'/*')) { + $found = true; + break; + } + } + } } if ($found) { $found = $caBundle; } return $found; } + + private static function validateCaFile($contents) + { + // assume the CA is valid if php is vulnerable to + // https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html + if ( + PHP_VERSION_ID <= 50327 + || (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422) + || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50506) + ) { + return !empty($contents); + } + + return (bool) openssl_x509_parse($contents); + } }