nextcloud/tests/lib/Http/Client/ClientTest.php
Lukas Reschke 5f3abffe6f Improve networking checks
Whilst we currently state that SSRF is generally outside of our threat model, this is something where we should invest to improve this.

Signed-off-by: Lukas Reschke <lukas@statuscode.ch>
2021-04-06 11:37:47 +00:00

600 lines
16 KiB
PHP

<?php
/**
* Copyright (c) 2015 Lukas Reschke <lukas@owncloud.com>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace Test\Http\Client;
use GuzzleHttp\Psr7\Response;
use OC\Http\Client\Client;
use OC\Http\Client\LocalAddressChecker;
use OC\Security\CertificateManager;
use OCP\Http\Client\LocalServerException;
use OCP\ICertificateManager;
use OCP\IConfig;
use OCP\ILogger;
use PHPUnit\Framework\MockObject\MockObject;
/**
* Class ClientTest
*/
class ClientTest extends \Test\TestCase {
/** @var \GuzzleHttp\Client|MockObject */
private $guzzleClient;
/** @var CertificateManager|MockObject */
private $certificateManager;
/** @var Client */
private $client;
/** @var IConfig|MockObject */
private $config;
/** @var ILogger|MockObject */
private $logger;
/** @var LocalAddressChecker|MockObject */
private $localAddressChecker;
/** @var array */
private $defaultRequestOptions;
protected function setUp(): void {
parent::setUp();
$this->config = $this->createMock(IConfig::class);
$this->logger = $this->createMock(ILogger::class);
$this->guzzleClient = $this->createMock(\GuzzleHttp\Client::class);
$this->certificateManager = $this->createMock(ICertificateManager::class);
$this->localAddressChecker = $this->createMock(LocalAddressChecker::class);
$this->client = new Client(
$this->config,
$this->logger,
$this->certificateManager,
$this->guzzleClient,
$this->localAddressChecker
);
}
public function testGetProxyUri(): void {
$this->config
->method('getSystemValue')
->with('proxy', null)
->willReturn(null);
$this->assertNull(self::invokePrivate($this->client, 'getProxyUri'));
}
public function testGetProxyUriProxyHostEmptyPassword(): void {
$map = [
['proxy', '', 'foo'],
['proxyuserpwd', '', null],
['proxyexclude', [], []],
];
$this->config
->method('getSystemValue')
->will($this->returnValueMap($map));
$this->assertEquals([
'http' => 'foo',
'https' => 'foo'
], self::invokePrivate($this->client, 'getProxyUri'));
}
public function testGetProxyUriProxyHostWithPassword(): void {
$this->config
->expects($this->at(0))
->method('getSystemValue')
->with(
$this->equalTo('proxy'),
$this->callback(function ($input) {
return $input === '';
})
)
->willReturn('foo');
$this->config
->expects($this->at(1))
->method('getSystemValue')
->with(
$this->equalTo('proxyuserpwd'),
$this->callback(function ($input) {
return $input === '';
})
)
->willReturn('username:password');
$this->config
->expects($this->at(2))
->method('getSystemValue')
->with(
$this->equalTo('proxyexclude'),
$this->callback(function ($input) {
return $input === [];
})
)
->willReturn([]);
$this->assertEquals([
'http' => 'username:password@foo',
'https' => 'username:password@foo'
], self::invokePrivate($this->client, 'getProxyUri'));
}
public function testGetProxyUriProxyHostWithPasswordAndExclude(): void {
$this->config
->expects($this->at(0))
->method('getSystemValue')
->with(
$this->equalTo('proxy'),
$this->callback(function ($input) {
return $input === '';
})
)
->willReturn('foo');
$this->config
->expects($this->at(1))
->method('getSystemValue')
->with(
$this->equalTo('proxyuserpwd'),
$this->callback(function ($input) {
return $input === '';
})
)
->willReturn('username:password');
$this->config
->expects($this->at(2))
->method('getSystemValue')
->with(
$this->equalTo('proxyexclude'),
$this->callback(function ($input) {
return $input === [];
})
)
->willReturn(['bar']);
$this->assertEquals([
'http' => 'username:password@foo',
'https' => 'username:password@foo',
'no' => ['bar']
], self::invokePrivate($this->client, 'getProxyUri'));
}
public function dataPreventLocalAddress():array {
return [
['localhost/foo.bar'],
['localHost/foo.bar'],
['random-host/foo.bar'],
['[::1]/bla.blub'],
['[::]/bla.blub'],
['192.168.0.1'],
['172.16.42.1'],
['[fdf8:f53b:82e4::53]/secret.ics'],
['[fe80::200:5aee:feaa:20a2]/secret.ics'],
['[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
['10.0.0.1'],
['another-host.local'],
['service.localhost'],
['!@#$'], // test invalid url
];
}
/**
* @dataProvider dataPreventLocalAddress
* @param string $uri
*/
public function testPreventLocalAddressDisabledByGlobalConfig(string $uri): void {
$this->config->expects($this->once())
->method('getSystemValueBool')
->with('allow_local_remote_servers', false)
->willReturn(true);
// $this->expectException(LocalServerException::class);
self::invokePrivate($this->client, 'preventLocalAddress', ['http://' . $uri, []]);
}
/**
* @dataProvider dataPreventLocalAddress
* @param string $uri
*/
public function testPreventLocalAddressDisabledByOption(string $uri): void {
$this->config->expects($this->never())
->method('getSystemValueBool');
// $this->expectException(LocalServerException::class);
self::invokePrivate($this->client, 'preventLocalAddress', ['http://' . $uri, [
'nextcloud' => ['allow_local_address' => true],
]]);
}
/**
* @dataProvider dataPreventLocalAddress
* @param string $uri
*/
public function testPreventLocalAddressOnGet(string $uri): void {
$this->expectException(LocalServerException::class);
$this->localAddressChecker
->expects($this->once())
->method('ThrowIfLocalAddress')
->with('http://' . $uri)
->will($this->throwException(new LocalServerException()));
$this->client->get('http://' . $uri);
}
/**
* @dataProvider dataPreventLocalAddress
* @param string $uri
*/
public function testPreventLocalAddressOnHead(string $uri): void {
$this->expectException(LocalServerException::class);
$this->localAddressChecker
->expects($this->once())
->method('ThrowIfLocalAddress')
->with('http://' . $uri)
->will($this->throwException(new LocalServerException()));
$this->client->head('http://' . $uri);
}
/**
* @dataProvider dataPreventLocalAddress
* @param string $uri
*/
public function testPreventLocalAddressOnPost(string $uri): void {
$this->expectException(LocalServerException::class);
$this->localAddressChecker
->expects($this->once())
->method('ThrowIfLocalAddress')
->with('http://' . $uri)
->will($this->throwException(new LocalServerException()));
$this->client->post('http://' . $uri);
}
/**
* @dataProvider dataPreventLocalAddress
* @param string $uri
*/
public function testPreventLocalAddressOnPut(string $uri): void {
$this->expectException(LocalServerException::class);
$this->localAddressChecker
->expects($this->once())
->method('ThrowIfLocalAddress')
->with('http://' . $uri)
->will($this->throwException(new LocalServerException()));
$this->client->put('http://' . $uri);
}
/**
* @dataProvider dataPreventLocalAddress
* @param string $uri
*/
public function testPreventLocalAddressOnDelete(string $uri): void {
$this->expectException(LocalServerException::class);
$this->localAddressChecker
->expects($this->once())
->method('ThrowIfLocalAddress')
->with('http://' . $uri)
->will($this->throwException(new LocalServerException()));
$this->client->delete('http://' . $uri);
}
private function setUpDefaultRequestOptions(): void {
$map = [
['proxy', '', 'foo'],
['proxyuserpwd', '', null],
['proxyexclude', [], []],
];
$this->config
->method('getSystemValue')
->will($this->returnValueMap($map));
$this->config
->method('getSystemValueBool')
->with('allow_local_remote_servers', false)
->willReturn(true);
$this->certificateManager
->expects($this->once())
->method('getAbsoluteBundlePath')
->with()
->willReturn('/my/path.crt');
$this->defaultRequestOptions = [
'verify' => '/my/path.crt',
'proxy' => [
'http' => 'foo',
'https' => 'foo'
],
'headers' => [
'User-Agent' => 'Nextcloud Server Crawler',
'Accept-Encoding' => 'gzip',
],
'timeout' => 30,
'nextcloud' => [
'allow_local_address' => true,
],
];
}
public function testGet(): void {
$this->setUpDefaultRequestOptions();
$this->guzzleClient->method('request')
->with('get', 'http://localhost/', $this->defaultRequestOptions)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->get('http://localhost/', [])->getStatusCode());
}
public function testGetWithOptions(): void {
$this->setUpDefaultRequestOptions();
$options = array_merge($this->defaultRequestOptions, [
'verify' => false,
'proxy' => [
'http' => 'bar',
'https' => 'bar'
],
]);
$this->guzzleClient->method('request')
->with('get', 'http://localhost/', $options)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->get('http://localhost/', $options)->getStatusCode());
}
public function testPost(): void {
$this->setUpDefaultRequestOptions();
$this->guzzleClient->method('request')
->with('post', 'http://localhost/', $this->defaultRequestOptions)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->post('http://localhost/', [])->getStatusCode());
}
public function testPostWithOptions(): void {
$this->setUpDefaultRequestOptions();
$options = array_merge($this->defaultRequestOptions, [
'verify' => false,
'proxy' => [
'http' => 'bar',
'https' => 'bar'
],
]);
$this->guzzleClient->method('request')
->with('post', 'http://localhost/', $options)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->post('http://localhost/', $options)->getStatusCode());
}
public function testPut(): void {
$this->setUpDefaultRequestOptions();
$this->guzzleClient->method('request')
->with('put', 'http://localhost/', $this->defaultRequestOptions)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->put('http://localhost/', [])->getStatusCode());
}
public function testPutWithOptions(): void {
$this->setUpDefaultRequestOptions();
$options = array_merge($this->defaultRequestOptions, [
'verify' => false,
'proxy' => [
'http' => 'bar',
'https' => 'bar'
],
]);
$this->guzzleClient->method('request')
->with('put', 'http://localhost/', $options)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->put('http://localhost/', $options)->getStatusCode());
}
public function testDelete(): void {
$this->setUpDefaultRequestOptions();
$this->guzzleClient->method('request')
->with('delete', 'http://localhost/', $this->defaultRequestOptions)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->delete('http://localhost/', [])->getStatusCode());
}
public function testDeleteWithOptions(): void {
$this->setUpDefaultRequestOptions();
$options = array_merge($this->defaultRequestOptions, [
'verify' => false,
'proxy' => [
'http' => 'bar',
'https' => 'bar'
],
]);
$this->guzzleClient->method('request')
->with('delete', 'http://localhost/', $options)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->delete('http://localhost/', $options)->getStatusCode());
}
public function testOptions(): void {
$this->setUpDefaultRequestOptions();
$this->guzzleClient->method('request')
->with('options', 'http://localhost/', $this->defaultRequestOptions)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->options('http://localhost/', [])->getStatusCode());
}
public function testOptionsWithOptions(): void {
$this->setUpDefaultRequestOptions();
$options = array_merge($this->defaultRequestOptions, [
'verify' => false,
'proxy' => [
'http' => 'bar',
'https' => 'bar'
],
]);
$this->guzzleClient->method('request')
->with('options', 'http://localhost/', $options)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->options('http://localhost/', $options)->getStatusCode());
}
public function testHead(): void {
$this->setUpDefaultRequestOptions();
$this->guzzleClient->method('request')
->with('head', 'http://localhost/', $this->defaultRequestOptions)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->head('http://localhost/', [])->getStatusCode());
}
public function testHeadWithOptions(): void {
$this->setUpDefaultRequestOptions();
$options = array_merge($this->defaultRequestOptions, [
'verify' => false,
'proxy' => [
'http' => 'bar',
'https' => 'bar'
],
]);
$this->guzzleClient->method('request')
->with('head', 'http://localhost/', $options)
->willReturn(new Response(418));
$this->assertEquals(418, $this->client->head('http://localhost/', $options)->getStatusCode());
}
public function testSetDefaultOptionsWithNotInstalled(): void {
$this->config
->expects($this->at(1))
->method('getSystemValue')
->with('installed', false)
->willReturn(false);
$this->certificateManager
->expects($this->never())
->method('listCertificates');
$this->assertEquals([
'verify' => \OC::$SERVERROOT . '/resources/config/ca-bundle.crt',
'headers' => [
'User-Agent' => 'Nextcloud Server Crawler',
'Accept-Encoding' => 'gzip',
],
'timeout' => 30,
'nextcloud' => [
'allow_local_address' => false,
],
'allow_redirects' => [
'on_redirect' => function (
\Psr\Http\Message\RequestInterface $request,
\Psr\Http\Message\ResponseInterface $response,
\Psr\Http\Message\UriInterface $uri
) {
},
],
], self::invokePrivate($this->client, 'buildRequestOptions', [[]]));
}
public function testSetDefaultOptionsWithProxy(): void {
$this->config
->expects($this->at(0))
->method('getSystemValue')
->with('proxy', null)
->willReturn('foo');
$this->config
->expects($this->at(1))
->method('getSystemValue')
->with('proxyuserpwd', null)
->willReturn(null);
$this->config
->expects($this->at(2))
->method('getSystemValue')
->with('proxyexclude', [])
->willReturn([]);
$this->certificateManager
->expects($this->once())
->method('getAbsoluteBundlePath')
->with()
->willReturn('/my/path.crt');
$this->assertEquals([
'verify' => '/my/path.crt',
'proxy' => [
'http' => 'foo',
'https' => 'foo'
],
'headers' => [
'User-Agent' => 'Nextcloud Server Crawler',
'Accept-Encoding' => 'gzip',
],
'timeout' => 30,
'nextcloud' => [
'allow_local_address' => false,
],
'allow_redirects' => [
'on_redirect' => function (
\Psr\Http\Message\RequestInterface $request,
\Psr\Http\Message\ResponseInterface $response,
\Psr\Http\Message\UriInterface $uri
) {
},
],
], self::invokePrivate($this->client, 'buildRequestOptions', [[]]));
}
public function testSetDefaultOptionsWithProxyAndExclude(): void {
$this->config
->expects($this->at(0))
->method('getSystemValue')
->with('proxy', null)
->willReturn('foo');
$this->config
->expects($this->at(1))
->method('getSystemValue')
->with('proxyuserpwd', null)
->willReturn(null);
$this->config
->expects($this->at(2))
->method('getSystemValue')
->with('proxyexclude', [])
->willReturn(['bar']);
$this->certificateManager
->expects($this->once())
->method('getAbsoluteBundlePath')
->with()
->willReturn('/my/path.crt');
$this->assertEquals([
'verify' => '/my/path.crt',
'proxy' => [
'http' => 'foo',
'https' => 'foo',
'no' => ['bar']
],
'headers' => [
'User-Agent' => 'Nextcloud Server Crawler',
'Accept-Encoding' => 'gzip',
],
'timeout' => 30,
'nextcloud' => [
'allow_local_address' => false,
],
'allow_redirects' => [
'on_redirect' => function (
\Psr\Http\Message\RequestInterface $request,
\Psr\Http\Message\ResponseInterface $response,
\Psr\Http\Message\UriInterface $uri
) {
},
],
], self::invokePrivate($this->client, 'buildRequestOptions', [[]]));
}
}