Skip to content

Commit

Permalink
fix: add validation for the token URL and service account impersonati…
Browse files Browse the repository at this point in the history
…on 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
  • Loading branch information
lsirac committed Aug 17, 2021
1 parent 68bceba commit 23cb8ef
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 13 deletions.
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -179,6 +182,11 @@ protected ExternalAccountCredentials(
this.environmentProvider =
environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider;

validateTokenUrl(tokenUrl);
if (serviceAccountImpersonationUrl != null) {
validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
}

this.impersonatedCredentials = initializeImpersonatedCredentials();
}

Expand Down Expand Up @@ -420,6 +428,60 @@ EnvironmentProvider getEnvironmentProvider() {
return environmentProvider;
}

static void validateTokenUrl(String tokenUrl) {
List<Pattern> 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<Pattern> 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<Pattern> 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 {

Expand Down
Expand Up @@ -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";

Expand All @@ -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();
Expand Down Expand Up @@ -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)
Expand All @@ -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);
Expand Down
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -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);
Expand All @@ -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<String, Object>()),
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<String, Object>()),
/* 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 =
Expand Down Expand Up @@ -267,7 +311,7 @@ public void getRequestMetadata_withQuotaProjectId() throws IOException {
transportFactory,
"audience",
"subjectTokenType",
"tokenUrl",
STS_URL,
new TestCredentialSource(new HashMap<String, Object>()),
"tokenInfoUrl",
/* serviceAccountImpersonationUrl= */ null,
Expand All @@ -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<String> 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<String> 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<String> 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<String> 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");
Expand Down
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
Expand Up @@ -59,6 +59,8 @@
@RunWith(JUnit4.class)
public class IdentityPoolCredentialsTest {

private static final String STS_URL = "https://sts.googleapis.com";

private static final Map<String, Object> FILE_CREDENTIAL_SOURCE_MAP =
new HashMap<String, Object>() {
{
Expand All @@ -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();
Expand Down Expand Up @@ -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")
Expand All @@ -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);
Expand Down
Expand Up @@ -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";
Expand Down

0 comments on commit 23cb8ef

Please sign in to comment.