1
0
Fork 0

Disable git, svn, http protocols for VCS downloaders, fixes #4968

pull/4982/head
Jordi Boggiano 2016-03-01 13:19:44 +00:00
parent cc14bb3ba9
commit 6f42b9c865
10 changed files with 72 additions and 26 deletions

View File

@ -1,5 +1,6 @@
### [1.0.0-beta1] - 2016-XX-XX ### [1.0.0-beta1] - 2016-XX-XX
* Break: By default we now disable any non-secure protocols (http, git, svn). This may lead to issues if you rely on those. See `secure-http` config option.
* Added VCS repo support for the GitLab API, see also `gitlab-oauth` and `gitlab-domains` config options * Added VCS repo support for the GitLab API, see also `gitlab-oauth` and `gitlab-domains` config options
* Added `prohibits` / `why-not` command to show what blocks an upgrade to a given package:version pair * Added `prohibits` / `why-not` command to show what blocks an upgrade to a given package:version pair
* Added --tree / -t to the `show` command to see all your installed packages in a tree view * Added --tree / -t to the `show` command to see all your installed packages in a tree view

View File

@ -26,7 +26,7 @@ class Config
'use-include-path' => false, 'use-include-path' => false,
'preferred-install' => 'auto', 'preferred-install' => 'auto',
'notify-on-install' => true, 'notify-on-install' => true,
'github-protocols' => array('git', 'https', 'ssh'), 'github-protocols' => array('https', 'ssh', 'git'),
'vendor-dir' => 'vendor', 'vendor-dir' => 'vendor',
'bin-dir' => '{$vendor-dir}/bin', 'bin-dir' => '{$vendor-dir}/bin',
'cache-dir' => '{$home}/cache', 'cache-dir' => '{$home}/cache',
@ -285,11 +285,15 @@ class Config
return $this->config[$key]; return $this->config[$key];
case 'github-protocols': case 'github-protocols':
if (reset($this->config['github-protocols']) === 'http') { $protos = $this->config['github-protocols'];
if ($this->config['secure-http'] && false !== ($index = array_search('git', $protos))) {
unset($protos[$index]);
}
if (reset($protos) === 'http') {
throw new \RuntimeException('The http protocol for github is not available anymore, update your config\'s github-protocols to use "https", "git" or "ssh"'); throw new \RuntimeException('The http protocol for github is not available anymore, update your config\'s github-protocols to use "https", "git" or "ssh"');
} }
return $this->config[$key]; return $protos;
case 'disable-tls': case 'disable-tls':
return $this->config[$key] !== 'false' && (bool) $this->config[$key]; return $this->config[$key] !== 'false' && (bool) $this->config[$key];

View File

@ -25,6 +25,8 @@ class HgDownloader extends VcsDownloader
*/ */
public function doDownload(PackageInterface $package, $path, $url) public function doDownload(PackageInterface $package, $path, $url)
{ {
$this->checkSecureHttp($url);
$url = ProcessExecutor::escape($url); $url = ProcessExecutor::escape($url);
$ref = ProcessExecutor::escape($package->getSourceReference()); $ref = ProcessExecutor::escape($package->getSourceReference());
$this->io->writeError(" Cloning ".$package->getSourceReference()); $this->io->writeError(" Cloning ".$package->getSourceReference());
@ -43,6 +45,8 @@ class HgDownloader extends VcsDownloader
*/ */
public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
{ {
$this->checkSecureHttp($url);
$url = ProcessExecutor::escape($url); $url = ProcessExecutor::escape($url);
$ref = ProcessExecutor::escape($target->getSourceReference()); $ref = ProcessExecutor::escape($target->getSourceReference());
$this->io->writeError(" Updating to ".$target->getSourceReference()); $this->io->writeError(" Updating to ".$target->getSourceReference());
@ -85,6 +89,13 @@ class HgDownloader extends VcsDownloader
return $output; return $output;
} }
protected function checkSecureHttp($url)
{
if (preg_match('{^http:}i', $url) && $this->config->get('secure-http')) {
throw new TransportException("Your configuration does not allow connection to $url. See https://getcomposer.org/doc/06-config.md#secure-http for details.");
}
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -69,6 +69,10 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
$this->doDownload($package, $path, $url); $this->doDownload($package, $path, $url);
break; break;
} catch (\Exception $e) { } catch (\Exception $e) {
// rethrow phpunit exceptions to avoid hard to debug bug failures
if ($e instanceof \PHPUnit_Framework_Exception) {
throw $e;
}
if ($this->io->isDebug()) { if ($this->io->isDebug()) {
$this->io->writeError('Failed: ['.get_class($e).'] '.$e->getMessage()); $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getMessage());
} elseif (count($urls)) { } elseif (count($urls)) {

View File

@ -47,6 +47,10 @@ class HgDriver extends VcsDriver
throw new \RuntimeException('Can not clone '.$this->url.' to access package information. The "'.$cacheDir.'" directory is not writable by the current user.'); throw new \RuntimeException('Can not clone '.$this->url.' to access package information. The "'.$cacheDir.'" directory is not writable by the current user.');
} }
if (preg_match('{^http:}i', $this->url) && $this->config->get('secure-http')) {
throw new TransportException("Your configuration does not allow connection to $url. See https://getcomposer.org/doc/06-config.md#secure-http for details.");
}
// update the repo if it is a valid hg repository // update the repo if it is a valid hg repository
if (is_dir($this->repoDir) && 0 === $this->process->execute('hg summary', $output, $this->repoDir)) { if (is_dir($this->repoDir) && 0 === $this->process->execute('hg summary', $output, $this->repoDir)) {
if (0 !== $this->process->execute('hg pull', $output, $this->repoDir)) { if (0 !== $this->process->execute('hg pull', $output, $this->repoDir)) {

View File

@ -39,6 +39,10 @@ class Git
public function runCommand($commandCallable, $url, $cwd, $initialClone = false) public function runCommand($commandCallable, $url, $cwd, $initialClone = false)
{ {
if (preg_match('{^(http|git):}i', $url) && $this->config->get('secure-http')) {
throw new TransportException("Your configuration does not allow connection to $url. See https://getcomposer.org/doc/06-config.md#secure-http for details.");
}
if ($initialClone) { if ($initialClone) {
$origCwd = $cwd; $origCwd = $cwd;
$cwd = null; $cwd = null;
@ -60,21 +64,20 @@ class Git
if (!is_array($protocols)) { if (!is_array($protocols)) {
throw new \RuntimeException('Config value "github-protocols" must be an array, got '.gettype($protocols)); throw new \RuntimeException('Config value "github-protocols" must be an array, got '.gettype($protocols));
} }
// public github, autoswitch protocols // public github, autoswitch protocols
if (preg_match('{^(?:https?|git)://'.self::getGitHubDomainsRegex($this->config).'/(.*)}', $url, $match)) { if (preg_match('{^(?:https?|git)://'.self::getGitHubDomainsRegex($this->config).'/(.*)}', $url, $match)) {
$messages = array(); $messages = array();
foreach ($protocols as $protocol) { foreach ($protocols as $protocol) {
if ('ssh' === $protocol) { if ('ssh' === $protocol) {
$url = "git@" . $match[1] . ":" . $match[2]; $protoUrl = "git@" . $match[1] . ":" . $match[2];
} else { } else {
$url = $protocol ."://" . $match[1] . "/" . $match[2]; $protoUrl = $protocol ."://" . $match[1] . "/" . $match[2];
} }
if (0 === $this->process->execute(call_user_func($commandCallable, $url), $ignoredOutput, $cwd)) { if (0 === $this->process->execute(call_user_func($commandCallable, $protoUrl), $ignoredOutput, $cwd)) {
return; return;
} }
$messages[] = '- ' . $url . "\n" . preg_replace('#^#m', ' ', $this->process->getErrorOutput()); $messages[] = '- ' . $protoUrl . "\n" . preg_replace('#^#m', ' ', $this->process->getErrorOutput());
if ($initialClone) { if ($initialClone) {
$this->filesystem->removeDirectory($origCwd); $this->filesystem->removeDirectory($origCwd);
} }
@ -104,8 +107,8 @@ class Git
if ($this->io->hasAuthentication($match[1])) { if ($this->io->hasAuthentication($match[1])) {
$auth = $this->io->getAuthentication($match[1]); $auth = $this->io->getAuthentication($match[1]);
$url = 'https://'.rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@'.$match[1].'/'.$match[2].'.git'; $authUrl = 'https://'.rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@'.$match[1].'/'.$match[2].'.git';
$command = call_user_func($commandCallable, $url); $command = call_user_func($commandCallable, $authUrl);
if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) {
return; return;
} }
@ -137,9 +140,9 @@ class Git
} }
if ($auth) { if ($auth) {
$url = $match[1].rawurlencode($auth['username']).':'.rawurlencode($auth['password']).'@'.$match[2].$match[3]; $authUrl = $match[1].rawurlencode($auth['username']).':'.rawurlencode($auth['password']).'@'.$match[2].$match[3];
$command = call_user_func($commandCallable, $url); $command = call_user_func($commandCallable, $authUrl);
if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) {
$this->io->setAuthentication($match[2], $auth['username'], $auth['password']); $this->io->setAuthentication($match[2], $auth['username'], $auth['password']);
$authHelper = new AuthHelper($this->io, $this->config); $authHelper = new AuthHelper($this->io, $this->config);

View File

@ -99,6 +99,10 @@ class Svn
*/ */
public function execute($command, $url, $cwd = null, $path = null, $verbose = false) public function execute($command, $url, $cwd = null, $path = null, $verbose = false)
{ {
if (preg_match('{^(http|svn):}i', $url) && $this->config->get('secure-http')) {
throw new TransportException("Your configuration does not allow connection to $url. See https://getcomposer.org/doc/06-config.md#secure-http for details.");
}
$svnCommand = $this->getCommand($command, $url, $path); $svnCommand = $this->getCommand($command, $url, $path);
$output = null; $output = null;
$io = $this->io; $io = $this->io;

View File

@ -196,12 +196,22 @@ class ConfigTest extends \PHPUnit_Framework_TestCase
public function testOverrideGithubProtocols() public function testOverrideGithubProtocols()
{ {
$config = new Config(false); $config = new Config(false);
$config->merge(array('config' => array('github-protocols' => array('https', 'git')))); $config->merge(array('config' => array('github-protocols' => array('https', 'ssh'))));
$config->merge(array('config' => array('github-protocols' => array('https')))); $config->merge(array('config' => array('github-protocols' => array('https'))));
$this->assertEquals(array('https'), $config->get('github-protocols')); $this->assertEquals(array('https'), $config->get('github-protocols'));
} }
public function testGitDisabledByDefaultInGithubProtocols()
{
$config = new Config(false);
$config->merge(array('config' => array('github-protocols' => array('https', 'git'))));
$this->assertEquals(array('https'), $config->get('github-protocols'));
$config->merge(array('config' => array('secure-http' => false)));
$this->assertEquals(array('https', 'git'), $config->get('github-protocols'));
}
/** /**
* @group TLS * @group TLS
*/ */

View File

@ -123,13 +123,18 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('1.0.0')); ->will($this->returnValue('1.0.0'));
$processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
$expectedGitCommand = $this->winCompat("git clone --no-checkout 'git://github.com/mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git://github.com/mirrors/composer' && git fetch composer"); $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://github.com/mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/mirrors/composer' && git fetch composer");
$processExecutor->expects($this->at(0)) $processExecutor->expects($this->at(0))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand)) ->with($this->equalTo($expectedGitCommand))
->will($this->returnValue(1)); ->will($this->returnValue(1));
$expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://github.com/mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/mirrors/composer' && git fetch composer"); $processExecutor->expects($this->at(1))
->method('getErrorOutput')
->with()
->will($this->returnValue('Error1'));
$expectedGitCommand = $this->winCompat("git clone --no-checkout 'git@github.com:mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git@github.com:mirrors/composer' && git fetch composer");
$processExecutor->expects($this->at(2)) $processExecutor->expects($this->at(2))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand)) ->with($this->equalTo($expectedGitCommand))
@ -141,7 +146,7 @@ class GitDownloaderTest extends TestCase
->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$expectedGitCommand = $this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'"); $expectedGitCommand = $this->winCompat("git remote set-url --push origin 'https://github.com/composer/composer.git'");
$processExecutor->expects($this->at(4)) $processExecutor->expects($this->at(4))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
@ -164,15 +169,15 @@ class GitDownloaderTest extends TestCase
public function pushUrlProvider() public function pushUrlProvider()
{ {
return array( return array(
array('git', 'git@github.com:composer/composer.git'), array('ssh', 'git@github.com:composer/composer', 'https://github.com/composer/composer.git'),
array('https', 'https://github.com/composer/composer.git'), array('https', 'https://github.com/composer/composer', 'https://github.com/composer/composer.git'),
); );
} }
/** /**
* @dataProvider pushUrlProvider * @dataProvider pushUrlProvider
*/ */
public function testDownloadAndSetPushUrlUseCustomVariousProtocolsForGithub($protocol, $pushUrl) public function testDownloadAndSetPushUrlUseCustomVariousProtocolsForGithub($protocol, $url, $pushUrl)
{ {
$packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock = $this->getMock('Composer\Package\PackageInterface');
$packageMock->expects($this->any()) $packageMock->expects($this->any())
@ -189,7 +194,7 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('1.0.0')); ->will($this->returnValue('1.0.0'));
$processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
$expectedGitCommand = $this->winCompat("git clone --no-checkout '{$protocol}://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer '{$protocol}://github.com/composer/composer' && git fetch composer"); $expectedGitCommand = $this->winCompat("git clone --no-checkout '{$url}' 'composerPath' && cd 'composerPath' && git remote add composer '{$url}' && git fetch composer");
$processExecutor->expects($this->at(0)) $processExecutor->expects($this->at(0))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand)) ->with($this->equalTo($expectedGitCommand))
@ -252,7 +257,7 @@ class GitDownloaderTest extends TestCase
public function testUpdate() public function testUpdate()
{ {
$expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git fetch composer && git fetch --tags composer");
$packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock = $this->getMock('Composer\Package\PackageInterface');
$packageMock->expects($this->any()) $packageMock->expects($this->any())
@ -309,7 +314,7 @@ class GitDownloaderTest extends TestCase
*/ */
public function testUpdateThrowsRuntimeExceptionIfGitCommandFails() public function testUpdateThrowsRuntimeExceptionIfGitCommandFails()
{ {
$expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git fetch composer && git fetch --tags composer");
$packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock = $this->getMock('Composer\Package\PackageInterface');
$packageMock->expects($this->any()) $packageMock->expects($this->any())
@ -354,7 +359,7 @@ class GitDownloaderTest extends TestCase
$this->markTestIncomplete('This test is disabled until https://github.com/composer/composer/issues/4973 is resolved'); $this->markTestIncomplete('This test is disabled until https://github.com/composer/composer/issues/4973 is resolved');
$expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '' && git fetch composer && git fetch --tags composer"); $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '' && git fetch composer && git fetch --tags composer");
$expectedSecondGitUpdateCommand = $this->winCompat("git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); $expectedSecondGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git fetch composer && git fetch --tags composer");
$packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock = $this->getMock('Composer\Package\PackageInterface');
$packageMock->expects($this->any()) $packageMock->expects($this->any())

View File

@ -47,9 +47,9 @@ class SvnDriverTest extends TestCase
{ {
$console = $this->getMock('Composer\IO\IOInterface'); $console = $this->getMock('Composer\IO\IOInterface');
$output = "svn: OPTIONS of 'http://corp.svn.local/repo':"; $output = "svn: OPTIONS of 'https://corp.svn.local/repo':";
$output .= " authorization failed: Could not authenticate to server:"; $output .= " authorization failed: Could not authenticate to server:";
$output .= " rejected Basic challenge (http://corp.svn.local/)"; $output .= " rejected Basic challenge (https://corp.svn.local/)";
$process = $this->getMock('Composer\Util\ProcessExecutor'); $process = $this->getMock('Composer\Util\ProcessExecutor');
$process->expects($this->at(1)) $process->expects($this->at(1))
@ -63,7 +63,7 @@ class SvnDriverTest extends TestCase
->will($this->returnValue(0)); ->will($this->returnValue(0));
$repoConfig = array( $repoConfig = array(
'url' => 'http://till:secret@corp.svn.local/repo', 'url' => 'https://till:secret@corp.svn.local/repo',
); );
$svn = new SvnDriver($repoConfig, $console, $this->config, $process); $svn = new SvnDriver($repoConfig, $console, $this->config, $process);