From 23cb8ef778d012bbd452c1dfdac5f096d1af6c95 Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Tue, 17 Aug 2021 12:47:49 -0700 Subject: [PATCH] fix: add validation for the token URL and service account impersonation URL for Workload Identity Federation (#717) * fix: add validation for the token URL and service account impersonation URL in ExternalAccountCredentials * fix: review comment * fix: add test case --- .../oauth2/ExternalAccountCredentials.java | 62 +++++++ .../auth/oauth2/AwsCredentialsTest.java | 9 +- .../ExternalAccountCredentialsTest.java | 157 +++++++++++++++++- .../ITWorkloadIdentityFederationTest.java | 4 +- .../oauth2/IdentityPoolCredentialsTest.java | 10 +- ...ckExternalAccountCredentialsTransport.java | 2 +- 6 files changed, 231 insertions(+), 13 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 191f584de..162a5a554 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -48,8 +48,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.Executor; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nullable; /** @@ -179,6 +182,11 @@ protected ExternalAccountCredentials( this.environmentProvider = environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider; + validateTokenUrl(tokenUrl); + if (serviceAccountImpersonationUrl != null) { + validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl); + } + this.impersonatedCredentials = initializeImpersonatedCredentials(); } @@ -420,6 +428,60 @@ EnvironmentProvider getEnvironmentProvider() { return environmentProvider; } + static void validateTokenUrl(String tokenUrl) { + List patterns = new ArrayList<>(); + patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\.sts\\.googleapis\\.com$")); + patterns.add(Pattern.compile("^sts\\.googleapis\\.com$")); + patterns.add(Pattern.compile("^sts\\.[^\\.\\s\\/\\\\]+\\.googleapis\\.com$")); + patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\-sts\\.googleapis\\.com$")); + + if (!isValidUrl(patterns, tokenUrl)) { + throw new IllegalArgumentException("The provided token URL is invalid."); + } + } + + static void validateServiceAccountImpersonationInfoUrl(String serviceAccountImpersonationUrl) { + List patterns = new ArrayList<>(); + patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\.iamcredentials\\.googleapis\\.com$")); + patterns.add(Pattern.compile("^iamcredentials\\.googleapis\\.com$")); + patterns.add(Pattern.compile("^iamcredentials\\.[^\\.\\s\\/\\\\]+\\.googleapis\\.com$")); + patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\-iamcredentials\\.googleapis\\.com$")); + + if (!isValidUrl(patterns, serviceAccountImpersonationUrl)) { + throw new IllegalArgumentException( + "The provided service account impersonation URL is invalid."); + } + } + + /** + * Returns true if the provided URL's scheme is HTTPS and the host comforms to at least one of the + * provided patterns. + */ + private static boolean isValidUrl(List patterns, String url) { + URI uri; + + try { + uri = URI.create(url); + } catch (Exception e) { + return false; + } + + // Scheme must be https and host must not be null. + if (uri.getScheme() == null + || uri.getHost() == null + || !"https".equals(uri.getScheme().toLowerCase(Locale.US))) { + return false; + } + + for (Pattern pattern : patterns) { + Matcher match = pattern.matcher(uri.getHost().toLowerCase(Locale.US)); + if (match.matches()) { + return true; + } + } + return false; + } + /** Base builder for external account credentials. */ public abstract static class Builder extends GoogleCredentials.Builder { diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index 1721fc5c1..213052c4e 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -58,6 +58,8 @@ @RunWith(JUnit4.class) public class AwsCredentialsTest { + private static final String STS_URL = "https://sts.googleapis.com"; + private static final String GET_CALLER_IDENTITY_URL = "https://sts.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"; @@ -83,7 +85,7 @@ public class AwsCredentialsTest { .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) .setAudience("audience") .setSubjectTokenType("subjectTokenType") - .setTokenUrl("tokenUrl") + .setTokenUrl(STS_URL) .setTokenInfoUrl("tokenInfoUrl") .setCredentialSource(AWS_CREDENTIAL_SOURCE) .build(); @@ -495,7 +497,8 @@ public void builder() { .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) .setAudience("audience") .setSubjectTokenType("subjectTokenType") - .setTokenUrl("tokenUrl") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") .setCredentialSource(AWS_CREDENTIAL_SOURCE) .setTokenInfoUrl("tokenInfoUrl") .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) @@ -507,7 +510,7 @@ public void builder() { assertEquals("audience", credentials.getAudience()); assertEquals("subjectTokenType", credentials.getSubjectTokenType()); - assertEquals(credentials.getTokenUrl(), "tokenUrl"); + assertEquals(credentials.getTokenUrl(), STS_URL); assertEquals(credentials.getTokenInfoUrl(), "tokenInfoUrl"); assertEquals( credentials.getServiceAccountImpersonationUrl(), SERVICE_ACCOUNT_IMPERSONATION_URL); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 3065d3420..9a0b4e180 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -44,10 +44,12 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; +import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import javax.annotation.Nullable; import org.junit.Before; @@ -59,7 +61,7 @@ @RunWith(JUnit4.class) public class ExternalAccountCredentialsTest { - private static final String STS_URL = "https://www.sts.google.com"; + private static final String STS_URL = "https://sts.googleapis.com"; static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory { @@ -176,7 +178,7 @@ public void fromJson_nullJson_throws() { @Test public void fromJson_invalidServiceAccountImpersonationUrl_throws() { GenericJson json = buildJsonIdentityPoolCredential(); - json.put("service_account_impersonation_url", "invalid_url"); + json.put("service_account_impersonation_url", "https://iamcredentials.googleapis.com"); try { ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY); @@ -199,6 +201,48 @@ public void fromJson_nullTransport_throws() { } } + @Test + public void constructor_invalidTokenUrl() { + try { + new TestExternalAccountCredentials( + transportFactory, + "audience", + "subjectTokenType", + "tokenUrl", + new TestCredentialSource(new HashMap()), + STS_URL, + /* serviceAccountImpersonationUrl= */ null, + "quotaProjectId", + /* clientId= */ null, + /* clientSecret= */ null, + /* scopes= */ null); + fail("Should have failed since an invalid token URL was passed."); + } catch (IllegalArgumentException e) { + assertEquals("The provided token URL is invalid.", e.getMessage()); + } + } + + @Test + public void constructor_invalidServiceAccountImpersonationUrl() { + try { + new TestExternalAccountCredentials( + transportFactory, + "audience", + "subjectTokenType", + "tokenUrl", + new TestCredentialSource(new HashMap()), + /* tokenInfoUrl= */ null, + "serviceAccountImpersonationUrl", + "quotaProjectId", + /* clientId= */ null, + /* clientSecret= */ null, + /* scopes= */ null); + fail("Should have failed since an invalid token URL was passed."); + } catch (IllegalArgumentException e) { + assertEquals("The provided token URL is invalid.", e.getMessage()); + } + } + @Test public void exchangeExternalCredentialForAccessToken() throws IOException { ExternalAccountCredentials credential = @@ -267,7 +311,7 @@ public void getRequestMetadata_withQuotaProjectId() throws IOException { transportFactory, "audience", "subjectTokenType", - "tokenUrl", + STS_URL, new TestCredentialSource(new HashMap()), "tokenInfoUrl", /* serviceAccountImpersonationUrl= */ null, @@ -282,6 +326,113 @@ public void getRequestMetadata_withQuotaProjectId() throws IOException { assertEquals("quotaProjectId", requestMetadata.get("x-goog-user-project").get(0)); } + @Test + public void validateTokenUrl_validUrls() { + List validUrls = + Arrays.asList( + "https://sts.googleapis.com", + "https://us-east-1.sts.googleapis.com", + "https://US-EAST-1.sts.googleapis.com", + "https://sts.us-east-1.googleapis.com", + "https://sts.US-WEST-1.googleapis.com", + "https://us-east-1-sts.googleapis.com", + "https://US-WEST-1-sts.googleapis.com", + "https://us-west-1-sts.googleapis.com/path?query"); + + for (String url : validUrls) { + ExternalAccountCredentials.validateTokenUrl(url); + ExternalAccountCredentials.validateTokenUrl(url.toUpperCase(Locale.US)); + } + } + + @Test + public void validateTokenUrl_invalidUrls() { + List invalidUrls = + Arrays.asList( + "https://iamcredentials.googleapis.com", + "sts.googleapis.com", + "https://", + "http://sts.googleapis.com", + "https://st.s.googleapis.com", + "https://us-eas\\t-1.sts.googleapis.com", + "https:/us-east-1.sts.googleapis.com", + "https://US-WE/ST-1-sts.googleapis.com", + "https://sts-us-east-1.googleapis.com", + "https://sts-US-WEST-1.googleapis.com", + "testhttps://us-east-1.sts.googleapis.com", + "https://us-east-1.sts.googleapis.comevil.com", + "https://us-east-1.us-east-1.sts.googleapis.com", + "https://us-ea.s.t.sts.googleapis.com", + "https://sts.googleapis.comevil.com", + "hhttps://us-east-1.sts.googleapis.com", + "https://us- -1.sts.googleapis.com", + "https://-sts.googleapis.com", + "https://us-east-1.sts.googleapis.com.evil.com"); + + for (String url : invalidUrls) { + try { + ExternalAccountCredentials.validateTokenUrl(url); + fail("Should have failed since an invalid URL was passed."); + } catch (IllegalArgumentException e) { + assertEquals("The provided token URL is invalid.", e.getMessage()); + } + } + } + + @Test + public void validateServiceAccountImpersonationUrls_validUrls() { + List validUrls = + Arrays.asList( + "https://iamcredentials.googleapis.com", + "https://us-east-1.iamcredentials.googleapis.com", + "https://US-EAST-1.iamcredentials.googleapis.com", + "https://iamcredentials.us-east-1.googleapis.com", + "https://iamcredentials.US-WEST-1.googleapis.com", + "https://us-east-1-iamcredentials.googleapis.com", + "https://US-WEST-1-iamcredentials.googleapis.com", + "https://us-west-1-iamcredentials.googleapis.com/path?query"); + + for (String url : validUrls) { + ExternalAccountCredentials.validateServiceAccountImpersonationInfoUrl(url); + ExternalAccountCredentials.validateServiceAccountImpersonationInfoUrl( + url.toUpperCase(Locale.US)); + } + } + + @Test + public void validateServiceAccountImpersonationUrls_invalidUrls() { + List invalidUrls = + Arrays.asList( + "https://sts.googleapis.com", + "iamcredentials.googleapis.com", + "https://", + "http://iamcredentials.googleapis.com", + "https://iamcre.dentials.googleapis.com", + "https://us-eas\t-1.iamcredentials.googleapis.com", + "https:/us-east-1.iamcredentials.googleapis.com", + "https://US-WE/ST-1-iamcredentials.googleapis.com", + "https://iamcredentials-us-east-1.googleapis.com", + "https://iamcredentials-US-WEST-1.googleapis.com", + "testhttps://us-east-1.iamcredentials.googleapis.com", + "https://us-east-1.iamcredentials.googleapis.comevil.com", + "https://us-east-1.us-east-1.iamcredentials.googleapis.com", + "https://us-ea.s.t.iamcredentials.googleapis.com", + "https://iamcredentials.googleapis.comevil.com", + "hhttps://us-east-1.iamcredentials.googleapis.com", + "https://us- -1.iamcredentials.googleapis.com", + "https://-iamcredentials.googleapis.com", + "https://us-east-1.iamcredentials.googleapis.com.evil.com"); + + for (String url : invalidUrls) { + try { + ExternalAccountCredentials.validateServiceAccountImpersonationInfoUrl(url); + fail("Should have failed since an invalid URL was passed."); + } catch (IllegalArgumentException e) { + assertEquals("The provided service account impersonation URL is invalid.", e.getMessage()); + } + } + } + private GenericJson buildJsonIdentityPoolCredential() { GenericJson json = new GenericJson(); json.put("audience", "audience"); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java index 52b6ca269..cd4448ced 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java @@ -164,7 +164,7 @@ private GenericJson buildIdentityPoolCredentialConfig() throws IOException { config.put("type", "external_account"); config.put("audience", OIDC_AUDIENCE); config.put("subject_token_type", "urn:ietf:params:oauth:token-type:jwt"); - config.put("token_url", "https://sts.googleapis.com/v1beta/token"); + config.put("token_url", "https://sts.googleapis.com/v1/token"); config.put( "service_account_impersonation_url", String.format( @@ -183,7 +183,7 @@ private GenericJson buildAwsCredentialConfig() { config.put("type", "external_account"); config.put("audience", AWS_AUDIENCE); config.put("subject_token_type", "urn:ietf:params:aws:token-type:aws4_request"); - config.put("token_url", "https://sts.googleapis.com/v1beta/token"); + config.put("token_url", "https://sts.googleapis.com/v1/token"); config.put( "service_account_impersonation_url", String.format( diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 871edb043..d937ed1f6 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -59,6 +59,8 @@ @RunWith(JUnit4.class) public class IdentityPoolCredentialsTest { + private static final String STS_URL = "https://sts.googleapis.com"; + private static final Map FILE_CREDENTIAL_SOURCE_MAP = new HashMap() { { @@ -75,7 +77,7 @@ public class IdentityPoolCredentialsTest { .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) .setAudience("audience") .setSubjectTokenType("subjectTokenType") - .setTokenUrl("tokenUrl") + .setTokenUrl(STS_URL) .setTokenInfoUrl("tokenInfoUrl") .setCredentialSource(FILE_CREDENTIAL_SOURCE) .build(); @@ -422,9 +424,9 @@ public void builder() { .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) .setAudience("audience") .setSubjectTokenType("subjectTokenType") - .setTokenUrl("tokenUrl") - .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .setTokenUrl(STS_URL) .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) .setQuotaProjectId("quotaProjectId") .setClientId("clientId") @@ -434,7 +436,7 @@ public void builder() { assertEquals("audience", credentials.getAudience()); assertEquals("subjectTokenType", credentials.getSubjectTokenType()); - assertEquals(credentials.getTokenUrl(), "tokenUrl"); + assertEquals(credentials.getTokenUrl(), STS_URL); assertEquals(credentials.getTokenInfoUrl(), "tokenInfoUrl"); assertEquals( credentials.getServiceAccountImpersonationUrl(), SERVICE_ACCOUNT_IMPERSONATION_URL); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java index 49e2b88be..108900705 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java @@ -68,7 +68,7 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport { private static final String AWS_CREDENTIALS_URL = "https://www.aws-credentials.com"; private static final String AWS_REGION_URL = "https://www.aws-region.com"; private static final String METADATA_SERVER_URL = "https://www.metadata.google.com"; - private static final String STS_URL = "https://www.sts.google.com"; + private static final String STS_URL = "https://sts.googleapis.com"; private static final String SUBJECT_TOKEN = "subjectToken"; private static final String TOKEN_TYPE = "Bearer";