From d934aabb09ebfd642cb6154c4ecd44fe4c7de36e Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Sat, 12 Nov 2022 11:56:33 -0800 Subject: [PATCH] Added url validation for aws metadata endpoints in aws external account (#31626) * Added url validation for aws metadata endpoints in aws external account * addressing review comments * fix error message back * Fix broken test --- .../aws_external_account_credentials.cc | 32 +++ test/core/security/credentials_test.cc | 195 ++++++++++++------ test/cpp/client/credentials_test.cc | 5 +- 3 files changed, 167 insertions(+), 65 deletions(-) diff --git a/src/core/lib/security/credentials/external/aws_external_account_credentials.cc b/src/core/lib/security/credentials/external/aws_external_account_credentials.cc index 2f0ab425875..eec48b5a3bf 100644 --- a/src/core/lib/security/credentials/external/aws_external_account_credentials.cc +++ b/src/core/lib/security/credentials/external/aws_external_account_credentials.cc @@ -37,6 +37,7 @@ #include #include "src/core/lib/gprpp/env.h" +#include "src/core/lib/gprpp/host_port.h" #include "src/core/lib/http/httpcli_ssl_credentials.h" #include "src/core/lib/iomgr/closure.h" #include "src/core/lib/json/json.h" @@ -47,6 +48,9 @@ namespace grpc_core { namespace { +const char* awsEc2MetadataIpv4Address = "169.254.169.254"; +const char* awsEc2MetadataIpv6Address = "fd00:ec2::254"; + const char* kExpectedEnvironmentId = "aws1"; const char* kRegionEnvVar = "AWS_REGION"; @@ -73,6 +77,15 @@ std::string UrlEncode(const absl::string_view& s) { return result; } +bool ValidateAwsUrl(const std::string& urlString) { + absl::StatusOr url = URI::Parse(urlString); + if (!url.ok()) return false; + absl::string_view host; + absl::string_view port; + SplitHostPort(url->authority(), &host, &port); + return host == awsEc2MetadataIpv4Address || host == awsEc2MetadataIpv6Address; +} + } // namespace RefCountedPtr @@ -115,10 +128,22 @@ AwsExternalAccountCredentials::AwsExternalAccountCredentials( return; } region_url_ = it->second.string_value(); + if (!ValidateAwsUrl(region_url_)) { + *error = GRPC_ERROR_CREATE(absl::StrFormat( + "Invalid host for region_url field, expecting %s or %s.", + awsEc2MetadataIpv4Address, awsEc2MetadataIpv6Address)); + return; + } it = options.credential_source.object_value().find("url"); if (it != options.credential_source.object_value().end() && it->second.type() == Json::Type::STRING) { url_ = it->second.string_value(); + if (!ValidateAwsUrl(url_)) { + *error = GRPC_ERROR_CREATE(absl::StrFormat( + "Invalid host for url field, expecting %s or %s.", + awsEc2MetadataIpv4Address, awsEc2MetadataIpv6Address)); + return; + } } it = options.credential_source.object_value().find( "regional_cred_verification_url"); @@ -138,6 +163,13 @@ AwsExternalAccountCredentials::AwsExternalAccountCredentials( if (it != options.credential_source.object_value().end() && it->second.type() == Json::Type::STRING) { imdsv2_session_token_url_ = it->second.string_value(); + if (!ValidateAwsUrl(imdsv2_session_token_url_)) { + *error = GRPC_ERROR_CREATE(absl::StrFormat( + "Invalid host for imdsv2_session_token_url field, expecting %s or " + "%s.", + awsEc2MetadataIpv4Address, awsEc2MetadataIpv6Address)); + return; + } } } diff --git a/test/core/security/credentials_test.cc b/test/core/security/credentials_test.cc index 33293b9ac32..d90f283e629 100644 --- a/test/core/security/credentials_test.cc +++ b/test/core/security/credentials_test.cc @@ -278,16 +278,25 @@ const char aws_imdsv2_session_token[] = "imdsv2_session_token"; const char valid_aws_external_account_creds_options_credential_source[] = "{\"environment_id\":\"aws1\"," - "\"region_url\":\"https://foo.com:5555/region_url\"," - "\"url\":\"https://foo.com:5555/url\"," + "\"region_url\":\"https://169.254.169.254:5555/region_url\"," + "\"url\":\"https://169.254.169.254:5555/url\"," "\"regional_cred_verification_url\":\"https://foo.com:5555/" "regional_cred_verification_url_{region}\"}"; const char valid_aws_imdsv2_external_account_creds_options_credential_source[] = "{\"environment_id\":\"aws1\"," - "\"region_url\":\"https://foo.com:5555/region_url\"," - "\"url\":\"https://foo.com:5555/url\"," - "\"imdsv2_session_token_url\":\"https://foo.com:5555/" + "\"region_url\":\"http://169.254.169.254:5555/region_url\"," + "\"url\":\"https://169.254.169.254:5555/url\"," + "\"imdsv2_session_token_url\":\"https://169.254.169.254/" + "imdsv2_session_token_url\"," + "\"regional_cred_verification_url\":\"https://foo.com:5555/" + "regional_cred_verification_url_{region}\"}"; + +const char valid_aws_external_account_creds_options_credential_source_ipv6[] = + "{\"environment_id\":\"aws1\"," + "\"region_url\":\"https://[fd00:ec2::254]:5555/region_url\"," + "\"url\":\"http://[fd00:ec2::254]:5555/url\"," + "\"imdsv2_session_token_url\":\"https://[fd00:ec2::254]/" "imdsv2_session_token_url\"," "\"regional_cred_verification_url\":\"https://foo.com:5555/" "regional_cred_verification_url_{region}\"}"; @@ -295,43 +304,53 @@ const char valid_aws_imdsv2_external_account_creds_options_credential_source[] = const char invalid_aws_external_account_creds_options_credential_source_unmatched_environment_id [] = "{\"environment_id\":\"unsupported_aws_version\"," - "\"region_url\":\"https://foo.com:5555/region_url\"," - "\"url\":\"https://foo.com:5555/url\"," + "\"region_url\":\"https://169.254.169.254:5555/region_url\"," + "\"url\":\"https://169.254.169.254:5555/url\"," "\"regional_cred_verification_url\":\"https://foo.com:5555/" "regional_cred_verification_url_{region}\"}"; const char - invalid_aws_external_account_creds_options_credential_source_invalid_region_url + invalid_aws_external_account_creds_options_credential_source_invalid_region_url_host [] = "{\"environment_id\":\"aws1\"," - "\"region_url\":\"invalid_region_url\"," - "\"url\":\"https://foo.com:5555/url\"," + "\"region_url\":\"https://fakeurl.com/url\"," + "\"url\":\"https://169.254.169.254:5555/url\"," "\"regional_cred_verification_url\":\"https://foo.com:5555/" "regional_cred_verification_url_{region}\"}"; const char - invalid_aws_external_account_creds_options_credential_source_invalid_url[] = - "{\"environment_id\":\"aws1\"," - "\"region_url\":\"https://foo.com:5555/region_url\"," - "\"url\":\"invalid_url\"," - "\"regional_cred_verification_url\":\"https://foo.com:5555/" - "regional_cred_verification_url_{region}\"}"; + invalid_aws_external_account_creds_options_credential_source_invalid_url_host + [] = "{\"environment_id\":\"aws1\"," + "\"region_url\":\"https://169.254.169.254:5555/region_url\"," + "\"url\":\"https://fake.com/url\"," + "\"regional_cred_verification_url\":\"https://foo.com:5555/" + "regional_cred_verification_url_{region}\"}"; const char invalid_aws_external_account_creds_options_credential_source_missing_role_name [] = "{\"environment_id\":\"aws1\"," - "\"region_url\":\"https://foo.com:5555/region_url\"," - "\"url\":\"https://foo.com:5555/url_no_role_name\"," + "\"region_url\":\"https://169.254.169.254:5555/region_url\"," + "\"url\":\"https://169.254.169.254:5555/url_no_role_name\"," "\"regional_cred_verification_url\":\"https://foo.com:5555/" "regional_cred_verification_url_{region}\"}"; const char invalid_aws_external_account_creds_options_credential_source_invalid_regional_cred_verification_url [] = "{\"environment_id\":\"aws1\"," - "\"region_url\":\"https://foo.com:5555/region_url\"," - "\"url\":\"https://foo.com:5555/url_no_role_name\"," + "\"region_url\":\"https://169.254.169.254:5555/region_url\"," + "\"url\":\"https://169.254.169.254:5555/url_no_role_name\"," "\"regional_cred_verification_url\":\"invalid_regional_cred_" "verification_url\"}"; +const char + invalid_aws_external_account_creds_options_credential_source_invalid_imdsv2_url_host + [] = "{\"environment_id\":\"aws1\"," + "\"region_url\":\"https://169.254.169.254:5555/region_url\"," + "\"url\":\"https://169.254.169.254:5555/url\"," + "\"imdsv2_session_token_url\":\"https://foo.com/" + "imdsv2_session_token_url\"," + "\"regional_cred_verification_url\":\"https://foo.com:5555/" + "regional_cred_verification_url_{region}\"}"; + /* -- Global state flags. -- */ bool g_test_is_on_gce = false; @@ -3030,6 +3049,41 @@ TEST(CredentialsTest, TestAwsImdsv2ExternalAccountCredsSuccess) { HttpRequest::SetOverride(nullptr, nullptr, nullptr); } +TEST(CredentialsTest, TestAwsExternalAccountCredsSuccessIpv6) { + ExecCtx exec_ctx; + auto credential_source = Json::Parse( + valid_aws_external_account_creds_options_credential_source_ipv6); + GPR_ASSERT(credential_source.ok()); + ExternalAccountCredentials::Options options = { + "external_account", // type; + "audience", // audience; + "subject_token_type", // subject_token_type; + "", // service_account_impersonation_url; + "https://foo.com:5555/token", // token_url; + "https://foo.com:5555/token_info", // token_info_url; + *credential_source, // credential_source; + "quota_project_id", // quota_project_id; + "client_id", // client_id; + "client_secret", // client_secret; + "", // workforce_pool_user_project; + }; + grpc_error_handle error; + auto creds = AwsExternalAccountCredentials::Create(options, {}, &error); + GPR_ASSERT(creds != nullptr); + GPR_ASSERT(error.ok()); + GPR_ASSERT(creds->min_security_level() == GRPC_PRIVACY_AND_INTEGRITY); + auto state = RequestMetadataState::NewInstance( + absl::OkStatus(), "authorization: Bearer token_exchange_access_token"); + HttpRequest::SetOverride( + aws_imdsv2_external_account_creds_httpcli_get_success, + aws_external_account_creds_httpcli_post_success, + aws_imdsv2_external_account_creds_httpcli_put_success); + state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority, + kTestPath); + ExecCtx::Get()->Flush(); + HttpRequest::SetOverride(nullptr, nullptr, nullptr); +} + TEST(CredentialsTest, TestAwsExternalAccountCredsSuccessPathRegionEnvKeysUrl) { ExecCtx exec_ctx; SetEnv("AWS_REGION", "test_regionz"); @@ -3344,10 +3398,10 @@ TEST(CredentialsTest, GPR_ASSERT(expected_error == actual_error); } -TEST(CredentialsTest, TestAwsExternalAccountCredsFailureInvalidRegionUrl) { +TEST(CredentialsTest, TestAwsExternalAccountCredsFailureMissingRoleName) { ExecCtx exec_ctx; auto credential_source = Json::Parse( - invalid_aws_external_account_creds_options_credential_source_invalid_region_url); + invalid_aws_external_account_creds_options_credential_source_missing_role_name); GPR_ASSERT(credential_source.ok()); ExternalAccountCredentials::Options options = { "external_account", // type; @@ -3367,7 +3421,7 @@ TEST(CredentialsTest, TestAwsExternalAccountCredsFailureInvalidRegionUrl) { GPR_ASSERT(creds != nullptr); GPR_ASSERT(error.ok()); GPR_ASSERT(creds->min_security_level() == GRPC_PRIVACY_AND_INTEGRITY); - error = GRPC_ERROR_CREATE("Invalid region url: invalid_region_url."); + error = GRPC_ERROR_CREATE("Missing role name when retrieving signing keys."); grpc_error_handle expected_error = GRPC_ERROR_CREATE_REFERENCING( "Error occurred when fetching oauth2 token.", &error, 1); auto state = RequestMetadataState::NewInstance(expected_error, {}); @@ -3380,10 +3434,11 @@ TEST(CredentialsTest, TestAwsExternalAccountCredsFailureInvalidRegionUrl) { HttpRequest::SetOverride(nullptr, nullptr, nullptr); } -TEST(CredentialsTest, TestAwsExternalAccountCredsFailureInvalidUrl) { +TEST(CredentialsTest, + TestAwsExternalAccountCredsFailureInvalidRegionalCredVerificationUrl) { ExecCtx exec_ctx; auto credential_source = Json::Parse( - invalid_aws_external_account_creds_options_credential_source_invalid_url); + invalid_aws_external_account_creds_options_credential_source_invalid_regional_cred_verification_url); GPR_ASSERT(credential_source.ok()); ExternalAccountCredentials::Options options = { "external_account", // type; @@ -3403,7 +3458,7 @@ TEST(CredentialsTest, TestAwsExternalAccountCredsFailureInvalidUrl) { GPR_ASSERT(creds != nullptr); GPR_ASSERT(error.ok()); GPR_ASSERT(creds->min_security_level() == GRPC_PRIVACY_AND_INTEGRITY); - error = GRPC_ERROR_CREATE("Invalid url: invalid_url."); + error = GRPC_ERROR_CREATE("Creating aws request signer failed."); grpc_error_handle expected_error = GRPC_ERROR_CREATE_REFERENCING( "Error occurred when fetching oauth2 token.", &error, 1); auto state = RequestMetadataState::NewInstance(expected_error, {}); @@ -3416,10 +3471,9 @@ TEST(CredentialsTest, TestAwsExternalAccountCredsFailureInvalidUrl) { HttpRequest::SetOverride(nullptr, nullptr, nullptr); } -TEST(CredentialsTest, TestAwsExternalAccountCredsFailureMissingRoleName) { - ExecCtx exec_ctx; +TEST(CredentialsTest, TestAwsExternalAccountCredsFailureInvalidUrlHost) { auto credential_source = Json::Parse( - invalid_aws_external_account_creds_options_credential_source_missing_role_name); + invalid_aws_external_account_creds_options_credential_source_invalid_url_host); GPR_ASSERT(credential_source.ok()); ExternalAccountCredentials::Options options = { "external_account", // type; @@ -3436,27 +3490,18 @@ TEST(CredentialsTest, TestAwsExternalAccountCredsFailureMissingRoleName) { }; grpc_error_handle error; auto creds = AwsExternalAccountCredentials::Create(options, {}, &error); - GPR_ASSERT(creds != nullptr); - GPR_ASSERT(error.ok()); - GPR_ASSERT(creds->min_security_level() == GRPC_PRIVACY_AND_INTEGRITY); - error = GRPC_ERROR_CREATE("Missing role name when retrieving signing keys."); - grpc_error_handle expected_error = GRPC_ERROR_CREATE_REFERENCING( - "Error occurred when fetching oauth2 token.", &error, 1); - auto state = RequestMetadataState::NewInstance(expected_error, {}); - HttpRequest::SetOverride(aws_external_account_creds_httpcli_get_success, - aws_external_account_creds_httpcli_post_success, - httpcli_put_should_not_be_called); - state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority, - kTestPath); - ExecCtx::Get()->Flush(); - HttpRequest::SetOverride(nullptr, nullptr, nullptr); + GPR_ASSERT(creds == nullptr); + std::string expected_error = + "Invalid host for url field, expecting 169.254.169.254 or fd00:ec2::254."; + std::string actual_error; + GPR_ASSERT(grpc_error_get_str(error, StatusStrProperty::kDescription, + &actual_error)); + GPR_ASSERT(expected_error == actual_error); } -TEST(CredentialsTest, - TestAwsExternalAccountCredsFailureInvalidRegionalCredVerificationUrl) { - ExecCtx exec_ctx; +TEST(CredentialsTest, TestAwsExternalAccountCredsFailureInvalidRegionUrlHost) { auto credential_source = Json::Parse( - invalid_aws_external_account_creds_options_credential_source_invalid_regional_cred_verification_url); + invalid_aws_external_account_creds_options_credential_source_invalid_region_url_host); GPR_ASSERT(credential_source.ok()); ExternalAccountCredentials::Options options = { "external_account", // type; @@ -3473,20 +3518,43 @@ TEST(CredentialsTest, }; grpc_error_handle error; auto creds = AwsExternalAccountCredentials::Create(options, {}, &error); - GPR_ASSERT(creds != nullptr); - GPR_ASSERT(error.ok()); - GPR_ASSERT(creds->min_security_level() == GRPC_PRIVACY_AND_INTEGRITY); - error = GRPC_ERROR_CREATE("Creating aws request signer failed."); - grpc_error_handle expected_error = GRPC_ERROR_CREATE_REFERENCING( - "Error occurred when fetching oauth2 token.", &error, 1); - auto state = RequestMetadataState::NewInstance(expected_error, {}); - HttpRequest::SetOverride(aws_external_account_creds_httpcli_get_success, - aws_external_account_creds_httpcli_post_success, - httpcli_put_should_not_be_called); - state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority, - kTestPath); - ExecCtx::Get()->Flush(); - HttpRequest::SetOverride(nullptr, nullptr, nullptr); + GPR_ASSERT(creds == nullptr); + std::string expected_error = + "Invalid host for region_url field, expecting 169.254.169.254 or " + "fd00:ec2::254."; + std::string actual_error; + GPR_ASSERT(grpc_error_get_str(error, StatusStrProperty::kDescription, + &actual_error)); + GPR_ASSERT(expected_error == actual_error); +} + +TEST(CredentialsTest, TestAwsExternalAccountCredsFailureInvalidImdsv2UrlHost) { + auto credential_source = Json::Parse( + invalid_aws_external_account_creds_options_credential_source_invalid_imdsv2_url_host); + GPR_ASSERT(credential_source.ok()); + ExternalAccountCredentials::Options options = { + "external_account", // type; + "audience", // audience; + "subject_token_type", // subject_token_type; + "", // service_account_impersonation_url; + "https://foo.com:5555/token", // token_url; + "https://foo.com:5555/token_info", // token_info_url; + *credential_source, // credential_source; + "quota_project_id", // quota_project_id; + "client_id", // client_id; + "client_secret", // client_secret; + "", // workforce_pool_user_project; + }; + grpc_error_handle error; + auto creds = AwsExternalAccountCredentials::Create(options, {}, &error); + GPR_ASSERT(creds == nullptr); + std::string expected_error = + "Invalid host for imdsv2_session_token_url field, expecting " + "169.254.169.254 or fd00:ec2::254."; + std::string actual_error; + GPR_ASSERT(grpc_error_get_str(error, StatusStrProperty::kDescription, + &actual_error)); + GPR_ASSERT(expected_error == actual_error); } TEST(CredentialsTest, TestExternalAccountCredentialsCreateSuccess) { @@ -3529,8 +3597,9 @@ TEST(CredentialsTest, TestExternalAccountCredentialsCreateSuccess) { "url\":\"service_account_impersonation_url\",\"token_url\":\"https://" "foo.com:5555/token\",\"token_info_url\":\"https://foo.com:5555/" "token_info\",\"credential_source\":{\"environment_id\":\"aws1\"," - "\"region_url\":\"https://foo.com:5555/region_url\",\"url\":\"https://" - "foo.com:5555/url\",\"regional_cred_verification_url\":\"https://" + "\"region_url\":\"https://169.254.169.254:5555/" + "region_url\",\"url\":\"https://" + "169.254.169.254:5555/url\",\"regional_cred_verification_url\":\"https://" "foo.com:5555/regional_cred_verification_url_{region}\"}," "\"quota_project_id\":\"quota_" "project_id\",\"client_id\":\"client_id\",\"client_secret\":\"client_" diff --git a/test/cpp/client/credentials_test.cc b/test/cpp/client/credentials_test.cc index bbe793877ef..2f5d9f2a762 100644 --- a/test/cpp/client/credentials_test.cc +++ b/test/cpp/client/credentials_test.cc @@ -102,8 +102,9 @@ TEST(CredentialsTest, ExternalAccountCredentials) { "url\":\"service_account_impersonation_url\",\"token_url\":\"https://" "foo.com:5555/token\",\"token_info_url\":\"https://foo.com:5555/" "token_info\",\"credential_source\":{\"environment_id\":\"aws1\"," - "\"region_url\":\"https://foo.com:5555/region_url\",\"url\":\"https://" - "foo.com:5555/url\",\"regional_cred_verification_url\":\"https://" + "\"region_url\":\"https://169.254.169.254:5555/" + "region_url\",\"url\":\"https://" + "169.254.169.254:5555/url\",\"regional_cred_verification_url\":\"https://" "foo.com:5555/regional_cred_verification_url_{region}\"}," "\"quota_project_id\":\"quota_" "project_id\",\"client_id\":\"client_id\",\"client_secret\":\"client_"