diff --git a/.changes/next-release/bugfix-auth-59087.json b/.changes/next-release/bugfix-auth-59087.json new file mode 100644 index 000000000000..eb5019123fc8 --- /dev/null +++ b/.changes/next-release/bugfix-auth-59087.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "auth", + "description": "Require HTTPS when using bearer token authorization" +} diff --git a/awscli/botocore/auth.py b/awscli/botocore/auth.py index 21a8033d3d2c..1db7104eea2d 100644 --- a/awscli/botocore/auth.py +++ b/awscli/botocore/auth.py @@ -33,6 +33,7 @@ urlunsplit, ) from botocore.exceptions import ( + InvalidEndpointConfigurationError, NoAuthTokenError, NoCredentialsError, UnknownSignatureVersionError, @@ -876,6 +877,12 @@ def add_auth(self, request): if self.auth_token is None: raise NoAuthTokenError() + # Bearer tokens are sent in plaintext so must only be sent over TLS + if not request.url.startswith('https'): + raise InvalidEndpointConfigurationError( + msg='Bearer authorization requires an HTTPS endpoint.' + ) + auth_header = f'Bearer {self.auth_token.token}' if 'Authorization' in request.headers: del request.headers['Authorization'] diff --git a/tests/functional/botocore/test_bearer_auth.py b/tests/functional/botocore/test_bearer_auth.py index 683f68f66104..c1c983f67011 100644 --- a/tests/functional/botocore/test_bearer_auth.py +++ b/tests/functional/botocore/test_bearer_auth.py @@ -11,9 +11,12 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import copy + import botocore import pytest from botocore.config import Config +from botocore.exceptions import InvalidEndpointConfigurationError from tests import ClientHTTPStubber, patch_load_service_model from tests.functional.test_useragent import ( @@ -253,3 +256,25 @@ def test_user_agent_does_not_have_bearer_service_env_vars_feature_id( feature_ids = parse_registered_feature_ids(ua_string) assert "3" not in feature_ids + + +def test_bearer_auth_rejects_http_endpoint(monkeypatch, patched_session): + monkeypatch.setenv( + "AWS_BEARER_TOKEN_BEARER_SERVICE", "bearer-service-token" + ) + http_ruleset = copy.deepcopy(MOCK_RULESET) + http_ruleset["rules"][0]["endpoint"]["url"] = ( + "http://bearer-service.us-west-2.amazonaws.com/" + ) + patch_load_service_model( + patched_session, monkeypatch, MOCK_SERVICE_MODEL, http_ruleset + ) + client = patched_session.create_client( + "bearer-service", + region_name="us-west-2", + ) + with ClientHTTPStubber(client) as http_stubber: + http_stubber.add_response() + with pytest.raises(InvalidEndpointConfigurationError): + client.mock_operation(MockOpParam="mock-op-param-value") + assert len(http_stubber.requests) == 0 diff --git a/tests/unit/botocore/test_auth_bearer.py b/tests/unit/botocore/test_auth_bearer.py index f779a5f6e0dd..35d965fa4b82 100644 --- a/tests/unit/botocore/test_auth_bearer.py +++ b/tests/unit/botocore/test_auth_bearer.py @@ -13,7 +13,10 @@ import pytest from botocore.auth import BearerAuth from botocore.awsrequest import AWSRequest -from botocore.exceptions import NoAuthTokenError +from botocore.exceptions import ( + InvalidEndpointConfigurationError, + NoAuthTokenError, +) from botocore.tokens import FrozenAuthToken cases = [ @@ -43,12 +46,26 @@ "token": None, "expectedException": NoAuthTokenError, }, + { + "documentation": "Signer rejects http endpoints", + "url": "http://example.com", + "headers": {}, + "token": "mF_9.B5f-4.1JqM", + "expectedException": InvalidEndpointConfigurationError, + }, + { + "documentation": "Signer rejects http endpoints with existing header", + "url": "http://example.com/path?query=value", + "headers": {"Authorization": "Bearer foo"}, + "token": "mF_9.B5f-4.1JqM", + "expectedException": InvalidEndpointConfigurationError, + }, ] @pytest.mark.parametrize("test_case", cases) def test_bearer_auth(test_case): - url = "https://example.com" + url = test_case.get("url", "https://example.com") headers = test_case.get("headers", {}) request = AWSRequest(method="GET", url=url, headers=headers) @@ -69,3 +86,7 @@ def test_bearer_auth(test_case): elif expected_exception: with pytest.raises(expected_exception): bearer_auth.add_auth(request) + # The token must never be attached when signing fails + assert request.headers.get("Authorization") != ( + f"Bearer {test_case['token']}" + )