Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: validate urls for external accounts #1031

Merged
merged 13 commits into from
May 3, 2022
55 changes: 55 additions & 0 deletions google/auth/external_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import re

import six
from urllib3.util import parse_url

from google.auth import _helpers
from google.auth import credentials
Expand Down Expand Up @@ -114,6 +115,12 @@ def __init__(
self._default_scopes = default_scopes
self._workforce_pool_user_project = workforce_pool_user_project

Credentials.validate_token_url(token_url)
if service_account_impersonation_url:
Credentials.validate_service_account_impersonation_url(
service_account_impersonation_url
)

if self._client_id:
self._client_auth = utils.ClientAuthentication(
utils.ClientAuthType.basic, self._client_id, self._client_secret
Expand Down Expand Up @@ -413,3 +420,51 @@ def _initialize_impersonated_credentials(self):
quota_project_id=self._quota_project_id,
iam_endpoint_override=self._service_account_impersonation_url,
)

@staticmethod
def validate_token_url(token_url):
_TOKEN_URL_PATTERNS = [
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
"^[^\\.\\s\\/\\\\]+\\.sts\\.googleapis\\.com$",
"^sts\\.googleapis\\.com$",
"^sts\\.[^\\.\\s\\/\\\\]+\\.googleapis\\.com$",
"^[^\\.\\s\\/\\\\]+\\-sts\\.googleapis\\.com$",
]

if not Credentials.is_valid_url(_TOKEN_URL_PATTERNS, token_url):
raise ValueError("The provided token URL is invalid.")

@staticmethod
def validate_service_account_impersonation_url(url):
_SERVICE_ACCOUNT_IMPERSONATION_URL_PATTERNS = [
"^[^\\.\\s\\/\\\\]+\\.iamcredentials\\.googleapis\\.com$",
"^iamcredentials\\.googleapis\\.com$",
"^iamcredentials\\.[^\\.\\s\\/\\\\]+\\.googleapis\\.com$",
"^[^\\.\\s\\/\\\\]+\\-iamcredentials\\.googleapis\\.com$",
]

if not Credentials.is_valid_url(
_SERVICE_ACCOUNT_IMPERSONATION_URL_PATTERNS, url
):
raise ValueError(
"The provided service account impersonation URL is invalid."
)

@staticmethod
def is_valid_url(patterns, url):
"""
Returns True if the provided URL's scheme is HTTPS and the host comforms to at least one of the provided patterns.
"""
# Check specifically for whitespcaces:
# Some python3.6 will parse the space character into %20 and pass the regex check which shouldn't be passed
if not url or len(str(url).split()) > 1:
return False

try:
uri = parse_url(url)
except Exception:
return False

if not uri.scheme or uri.scheme != "https" or not uri.hostname:
return False

return any(re.compile(p).match(uri.hostname.lower()) for p in patterns)
128 changes: 127 additions & 1 deletion tests/test_external_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,110 @@ def assert_resource_manager_request_kwargs(
assert request_kwargs["headers"] == headers
assert "body" not in request_kwargs

def test_valid_token_url_shall_pass_validation(self):
valid_urls = [
"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 url in valid_urls:
# A valid url shouldn't throw exception and a None value should be returned
external_account.Credentials.validate_token_url(url)

def test_invalid_token_url_shall_throw_exceptions(self):
invalid_urls = [
"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 url in invalid_urls:
# An invalid url should throw a ValueError exception
with pytest.raises(ValueError) as excinfo:
external_account.Credentials.validate_token_url(url)

assert excinfo.match("The provided token URL is invalid.")

def test_valid_service_account_impersonation_url_shall_pass_validation(self):
valid_urls = [
"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 url in valid_urls:
# A valid url shouldn't throw exception and a None value should be returned
external_account.Credentials.validate_service_account_impersonation_url(url)

def test_invalid_service_account_impersonate_url_shall_throw_exceptions(self):
invalid_urls = [
"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 url in invalid_urls:
# An invalid url should throw a ValueError exception
with pytest.raises(ValueError) as excinfo:
external_account.Credentials.validate_service_account_impersonation_url(
url
)

assert excinfo.match(
"The provided service account impersonation URL is invalid."
)

def test_default_state(self):
credentials = self.make_credentials()
credentials = self.make_credentials(
service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL
)

# Token url and service account impersonation url should be set
assert credentials._token_url
assert credentials._service_account_impersonation_url
# Not token acquired yet
assert not credentials.token
assert not credentials.valid
Expand All @@ -289,6 +390,31 @@ def test_default_state(self):
assert credentials.requires_scopes
assert not credentials.quota_project_id

def test_invalid_token_url(self):
with pytest.raises(ValueError) as excinfo:
CredentialsImpl(
audience=self.AUDIENCE,
subject_token_type=self.SUBJECT_TOKEN_TYPE,
token_url="https:///v1/token",
credential_source=self.CREDENTIAL_SOURCE,
)

assert excinfo.match("The provided token URL is invalid.")

def test_invalid_service_account_impersonate_url(self):
with pytest.raises(ValueError) as excinfo:
CredentialsImpl(
audience=self.AUDIENCE,
subject_token_type=self.SUBJECT_TOKEN_TYPE,
token_url=self.TOKEN_URL,
credential_source=self.CREDENTIAL_SOURCE,
service_account_impersonation_url=12345, # create an exception by sending to parse url
)

assert excinfo.match(
"The provided service account impersonation URL is invalid."
)

def test_nonworkforce_with_workforce_pool_user_project(self):
with pytest.raises(ValueError) as excinfo:
CredentialsImpl(
Expand Down