From d0905bf9e89c3809ca3c07de300676e3c26a7fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=BCbner?= Date: Fri, 24 Jun 2022 18:05:56 +0200 Subject: [PATCH] Sanitize host name for AWS requests before signing in AWSAuthV4 transport (#2090) --- CHANGELOG.md | 2 +- src/Transport/AwsAuthV4.php | 23 ++++++++++++++++++++-- tests/Transport/AwsAuthV4Test.php | 32 +++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) mode change 100755 => 100644 src/Transport/AwsAuthV4.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f3c7e8ed4b..ae3a4ee672 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Changed `value` in `SetProcessor` to accept `mixed` instead of `string` by @franmomu [#2082](https://github.com/ruflin/Elastica/pull/2082) * Updated `Query::create` PHPDoc to include supported types and propagate it to callers by @franmomu [#2088](https://github.com/ruflin/Elastica/pull/2088) * Update some iterable types in PHPDoc to be more specific by @franmomu [#2092](https://github.com/ruflin/Elastica/pull/2092) - +* Update `AWSAuthV4 transport` to sanitize host name for AWS requests before signing [#2090](https://github.com/ruflin/Elastica/pull/2090) ### Deprecated * Deprecated `Elastica\Reindex::WAIT_FOR_COMPLETION_FALSE`, use a boolean as parameter instead by @franmomu [#2070](https://github.com/ruflin/Elastica/pull/2070) * Passing anything else than a boolean as 1st argument to `Reindex::setWaitForCompletion`, pass a boolean instead by @franmomu [#2070](https://github.com/ruflin/Elastica/pull/2070) diff --git a/src/Transport/AwsAuthV4.php b/src/Transport/AwsAuthV4.php old mode 100755 new mode 100644 index da25ed112a..04f8cdc017 --- a/src/Transport/AwsAuthV4.php +++ b/src/Transport/AwsAuthV4.php @@ -10,6 +10,7 @@ use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; +use GuzzleHttp\Psr7; use Psr\Http\Message\RequestInterface; class AwsAuthV4 extends Guzzle @@ -42,15 +43,33 @@ private function getSigningMiddleware(): callable : \getenv('AWS_REGION'); $signer = new SignatureV4('es', $region); $credProvider = $this->getCredentialProvider(); + $transport = $this; return Middleware::mapRequest(static function (RequestInterface $req) use ( $signer, - $credProvider + $credProvider, + $transport ) { - return $signer->signRequest($req, $credProvider()->wait()); + return $signer->signRequest($transport->sanitizeRequest($req), $credProvider()->wait()); }); } + private function sanitizeRequest(RequestInterface $request): RequestInterface + { + // Trailing dots are valid parts of DNS host names (see RFC 1034), + // but interferes with header signing where AWS expects a stripped host name. + if ('.' === \substr($request->getHeader('host')[0], -1)) { + $changes = ['set_headers' => ['host' => \rtrim($request->getHeader('host')[0], '.')]]; + if (\class_exists(Psr7\Utils::class)) { + $request = Psr7\Utils::modifyRequest($request, $changes); + } else { + $request = Psr7\modify_request($request, $changes); + } + } + + return $request; + } + private function getCredentialProvider(): callable { $connection = $this->getConnection(); diff --git a/tests/Transport/AwsAuthV4Test.php b/tests/Transport/AwsAuthV4Test.php index bd5991569b..bfe833e2ce 100644 --- a/tests/Transport/AwsAuthV4Test.php +++ b/tests/Transport/AwsAuthV4Test.php @@ -6,6 +6,7 @@ use Aws\Credentials\Credentials; use Aws\Sdk; use Elastica\Exception\Connection\GuzzleException; +use GuzzleHttp\Exception\ConnectException; /** * @internal @@ -198,4 +199,35 @@ public function testSignsWithEnvironmentalCredentials(): void ); } } + + /** + * @group unit + * @depends testSignsWithProvidedCredentials + */ + public function testStripsTrailingDotInHost(): void + { + $host = $this->_getHost(); + $hostWithTrailingDot = $host.'.'; + + $config = [ + 'persistent' => false, + 'transport' => 'AwsAuthV4', + 'aws_access_key_id' => 'foo', + 'aws_secret_access_key' => 'bar', + 'aws_session_token' => 'baz', + 'aws_region' => 'us-east-1', + 'host' => $hostWithTrailingDot, + ]; + $client = $this->_getClient($config); + + try { + $client->request('_stats'); + } catch (GuzzleException $e) { + $guzzleException = $e->getGuzzleException(); + $this->assertInstanceOf(ConnectException::class, $guzzleException); + $request = $guzzleException->getRequest(); + $this->assertSame($host, $request->getHeader('host')[0]); + $this->assertSame($hostWithTrailingDot, $request->getUri()->getHost()); + } + } }