Skip to content

Commit 640b3ad

Browse files
authored
fix: validate region parameter to prevent SSRF request redirection (#417)
The region parameter was interpolated directly into endpoint URL f-strings without validation, allowing crafted values like "x@attacker.com:443/#" to redirect SDK API calls — including SigV4-signed requests with credentials — to non-AWS hosts. Same vulnerability class as CVE-2026-22611. Defense-in-depth fix: 1. validate_region() in endpoints.py checks against regex \A[a-z]{2}(-[a-z]+)+-\d+\Z using \Z anchor to prevent newline bypass. Applied in get_data_plane_endpoint(), build_runtime_url(), AgentCoreRuntimeClient, and BrowserClient. 2. _validate_endpoint_url() verifies constructed URL hostname ends with .amazonaws.com. Also validates env var endpoint overrides. 3. Boto3-based clients (CodeInterpreter, IdentityClient, ResourcePolicyClient, MemoryControlPlaneClient) no longer pass redundant endpoint_url — boto3 resolves identical endpoints natively and includes its own region validation. Ref: V2177374595
1 parent b8dc751 commit 640b3ad

13 files changed

Lines changed: 492 additions & 412 deletions

File tree

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,81 @@
11
"""Endpoint utilities for BedrockAgentCore services."""
22

33
import os
4+
import re
5+
from urllib.parse import urlparse
46

57
# Environment-configurable constants with fallback defaults
68
DP_ENDPOINT_OVERRIDE = os.getenv("BEDROCK_AGENTCORE_DP_ENDPOINT")
79
CP_ENDPOINT_OVERRIDE = os.getenv("BEDROCK_AGENTCORE_CP_ENDPOINT")
810
DEFAULT_REGION = os.getenv("AWS_REGION", "us-west-2")
911

12+
# Regex for valid AWS region names (e.g., us-east-1, eu-west-2, cn-north-1, us-gov-west-1).
13+
# Uses \A and \Z anchors to prevent newline injection bypass that $ allows.
14+
_VALID_REGION_PATTERN = re.compile(r"\A[a-z]{2}(-[a-z]+)+-\d+\Z")
15+
16+
17+
class InvalidRegionError(ValueError):
18+
"""Raised when an invalid AWS region string is provided.
19+
20+
This prevents SSRF attacks where a crafted region value
21+
(e.g., ``x@attacker.com:443/#``) could redirect SDK API calls
22+
to non-AWS hosts.
23+
"""
24+
25+
26+
def validate_region(region: str) -> str:
27+
"""Validate that a region string is a well-formed AWS region name.
28+
29+
Args:
30+
region: The region string to validate.
31+
32+
Returns:
33+
The validated region string (unchanged).
34+
35+
Raises:
36+
InvalidRegionError: If the region does not match the expected pattern.
37+
"""
38+
if not isinstance(region, str) or not _VALID_REGION_PATTERN.match(region):
39+
raise InvalidRegionError(
40+
f"Invalid AWS region: {region!r}. Region must match pattern like 'us-east-1', 'eu-west-2', 'cn-north-1'."
41+
)
42+
return region
43+
44+
45+
def _validate_endpoint_url(url: str) -> str:
46+
"""Validate that a constructed endpoint URL resolves to an AWS host.
47+
48+
This is a defense-in-depth check that catches URL manipulation even if
49+
the region regex is somehow bypassed.
50+
51+
Args:
52+
url: The constructed endpoint URL.
53+
54+
Returns:
55+
The validated URL (unchanged).
56+
57+
Raises:
58+
InvalidRegionError: If the URL hostname does not end with an AWS domain.
59+
"""
60+
parsed = urlparse(url)
61+
hostname = parsed.hostname or ""
62+
_AWS_DOMAINS = (".amazonaws.com", ".amazonaws.com.cn", ".api.aws")
63+
if not any(hostname.endswith(d) for d in _AWS_DOMAINS):
64+
raise InvalidRegionError(f"Constructed endpoint resolves to non-AWS host: {hostname!r}")
65+
return url
66+
1067

1168
def get_data_plane_endpoint(region: str = DEFAULT_REGION) -> str:
12-
return DP_ENDPOINT_OVERRIDE or f"https://bedrock-agentcore.{region}.amazonaws.com"
69+
if DP_ENDPOINT_OVERRIDE:
70+
return _validate_endpoint_url(DP_ENDPOINT_OVERRIDE)
71+
validate_region(region)
72+
url = f"https://bedrock-agentcore.{region}.amazonaws.com"
73+
return _validate_endpoint_url(url)
1374

1475

1576
def get_control_plane_endpoint(region: str = DEFAULT_REGION) -> str:
16-
return CP_ENDPOINT_OVERRIDE or f"https://bedrock-agentcore-control.{region}.amazonaws.com"
77+
if CP_ENDPOINT_OVERRIDE:
78+
return _validate_endpoint_url(CP_ENDPOINT_OVERRIDE)
79+
validate_region(region)
80+
url = f"https://bedrock-agentcore-control.{region}.amazonaws.com"
81+
return _validate_endpoint_url(url)

src/bedrock_agentcore/memory/controlplane.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ def __init__(self, region_name: str = "us-west-2", environment: str = "prod"):
3333
self.region_name = region_name
3434
self.environment = environment
3535

36-
self.endpoint = os.getenv(
37-
"BEDROCK_AGENTCORE_CONTROL_ENDPOINT", f"https://bedrock-agentcore-control.{region_name}.amazonaws.com"
38-
)
39-
4036
service_name = os.getenv("BEDROCK_AGENTCORE_CONTROL_SERVICE", "bedrock-agentcore-control")
41-
self.client = boto3.client(service_name, region_name=self.region_name, endpoint_url=self.endpoint)
37+
cp_kwargs: dict = {"region_name": self.region_name}
38+
control_endpoint = os.getenv("BEDROCK_AGENTCORE_CONTROL_ENDPOINT")
39+
if control_endpoint:
40+
cp_kwargs["endpoint_url"] = control_endpoint
41+
self.client = boto3.client(service_name, **cp_kwargs)
42+
self.endpoint = self.client.meta.endpoint_url
4243

4344
logger.info("Initialized MemoryControlPlaneClient for %s in %s", environment, region_name)
4445

src/bedrock_agentcore/runtime/a2a.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ def build_runtime_url(agent_arn: str, region: Optional[str] = None) -> str:
7575
"""
7676
from urllib.parse import quote
7777

78+
from .._utils.endpoints import validate_region
79+
7880
if region is None:
7981
# ARN format: arn:aws:bedrock-agentcore:<region>:<account>:runtime/<id>
8082
parts = agent_arn.split(":")
@@ -83,6 +85,7 @@ def build_runtime_url(agent_arn: str, region: Optional[str] = None) -> str:
8385
else:
8486
raise ValueError(f"Cannot extract region from ARN: {agent_arn}")
8587

88+
validate_region(region)
8689
encoded_arn = quote(agent_arn, safe="")
8790
return f"https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded_arn}/invocations"
8891

src/bedrock_agentcore/runtime/agent_core_runtime_client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ def __init__(self, region: str, session: Optional[boto3.Session] = None) -> None
4343
session (Optional[boto3.Session]): Optional boto3 session. If not provided,
4444
a new session will be created using default credentials.
4545
"""
46+
from .._utils.endpoints import validate_region
47+
48+
validate_region(region)
4649
self.region = region
4750
self.logger = logging.getLogger(__name__)
4851

src/bedrock_agentcore/services/identity.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import boto3
1111
from pydantic import BaseModel
1212

13-
from bedrock_agentcore._utils.endpoints import get_control_plane_endpoint, get_data_plane_endpoint
13+
from bedrock_agentcore._utils.endpoints import CP_ENDPOINT_OVERRIDE, DP_ENDPOINT_OVERRIDE
1414

1515

1616
class TokenPoller(ABC):
@@ -75,12 +75,14 @@ class IdentityClient:
7575
def __init__(self, region: str):
7676
"""Initialize the identity client with the specified region."""
7777
self.region = region
78-
self.cp_client = boto3.client(
79-
"bedrock-agentcore-control", region_name=region, endpoint_url=get_control_plane_endpoint(region)
80-
)
81-
self.dp_client = boto3.client(
82-
"bedrock-agentcore", region_name=region, endpoint_url=get_data_plane_endpoint(region)
83-
)
78+
cp_kwargs: dict = {"region_name": region}
79+
if CP_ENDPOINT_OVERRIDE:
80+
cp_kwargs["endpoint_url"] = CP_ENDPOINT_OVERRIDE
81+
self.cp_client = boto3.client("bedrock-agentcore-control", **cp_kwargs)
82+
dp_kwargs: dict = {"region_name": region}
83+
if DP_ENDPOINT_OVERRIDE:
84+
dp_kwargs["endpoint_url"] = DP_ENDPOINT_OVERRIDE
85+
self.dp_client = boto3.client("bedrock-agentcore", **dp_kwargs)
8486
self.logger = logging.getLogger("bedrock_agentcore.identity_client")
8587

8688
def create_oauth2_credential_provider(self, req):

src/bedrock_agentcore/services/resource_policy.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import boto3
88

9-
from bedrock_agentcore._utils.endpoints import get_control_plane_endpoint
9+
from bedrock_agentcore._utils.endpoints import CP_ENDPOINT_OVERRIDE
1010

1111

1212
class ResourcePolicyClient:
@@ -19,11 +19,10 @@ class ResourcePolicyClient:
1919
def __init__(self, region: str):
2020
"""Initialize the client for the specified region."""
2121
self.region = region
22-
self.client = boto3.client(
23-
"bedrock-agentcore-control",
24-
region_name=region,
25-
endpoint_url=get_control_plane_endpoint(region),
26-
)
22+
cp_kwargs: dict = {"region_name": region}
23+
if CP_ENDPOINT_OVERRIDE:
24+
cp_kwargs["endpoint_url"] = CP_ENDPOINT_OVERRIDE
25+
self.client = boto3.client("bedrock-agentcore-control", **cp_kwargs)
2726
self.logger = logging.getLogger("bedrock_agentcore.resource_policy_client")
2827

2928
def put_resource_policy(self, resource_arn: str, policy: Union[str, dict]) -> dict:

src/bedrock_agentcore/tools/browser_client.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from bedrock_agentcore._utils.user_agent import build_user_agent_suffix
2323

24-
from .._utils.endpoints import get_control_plane_endpoint, get_data_plane_endpoint
24+
from .._utils.endpoints import get_data_plane_endpoint
2525
from .config import (
2626
BrowserExtension,
2727
Certificate,
@@ -68,6 +68,9 @@ def __init__(self, region: str, integration_source: Optional[str] = None) -> Non
6868
for telemetry (e.g., 'langchain', 'crewai'). Used to track
6969
customer acquisition from different integrations.
7070
"""
71+
from bedrock_agentcore._utils.endpoints import CP_ENDPOINT_OVERRIDE, DP_ENDPOINT_OVERRIDE, validate_region
72+
73+
validate_region(region)
7174
self.region = region
7275
self.logger = logging.getLogger(__name__)
7376
self.integration_source = integration_source
@@ -76,21 +79,17 @@ def __init__(self, region: str, integration_source: Optional[str] = None) -> Non
7679
user_agent_extra = build_user_agent_suffix(integration_source)
7780
client_config = Config(user_agent_extra=user_agent_extra)
7881

79-
# Control plane client for browser management
80-
self.control_plane_client = boto3.client(
81-
"bedrock-agentcore-control",
82-
region_name=region,
83-
endpoint_url=get_control_plane_endpoint(region),
84-
config=client_config,
85-
)
86-
87-
# Data plane client for session operations
88-
self.data_plane_client = boto3.client(
89-
"bedrock-agentcore",
90-
region_name=region,
91-
endpoint_url=get_data_plane_endpoint(region),
92-
config=client_config,
93-
)
82+
# Control plane client — let boto3 resolve endpoint natively.
83+
cp_kwargs: dict = {"region_name": region, "config": client_config}
84+
if CP_ENDPOINT_OVERRIDE:
85+
cp_kwargs["endpoint_url"] = CP_ENDPOINT_OVERRIDE
86+
self.control_plane_client = boto3.client("bedrock-agentcore-control", **cp_kwargs)
87+
88+
# Data plane client — same pattern.
89+
dp_kwargs: dict = {"region_name": region, "config": client_config}
90+
if DP_ENDPOINT_OVERRIDE:
91+
dp_kwargs["endpoint_url"] = DP_ENDPOINT_OVERRIDE
92+
self.data_plane_client = boto3.client("bedrock-agentcore", **dp_kwargs)
9493

9594
self._identifier = None
9695
self._session_id = None

src/bedrock_agentcore/tools/code_interpreter_client.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import boto3
1515
from botocore.config import Config
1616

17-
from bedrock_agentcore._utils.endpoints import get_control_plane_endpoint, get_data_plane_endpoint
17+
from bedrock_agentcore._utils.endpoints import CP_ENDPOINT_OVERRIDE, DP_ENDPOINT_OVERRIDE
1818
from bedrock_agentcore._utils.user_agent import build_user_agent_suffix
1919

2020
from .config import Certificate
@@ -102,21 +102,18 @@ def __init__(
102102
# Data plane config (preserve existing read_timeout)
103103
data_config = Config(read_timeout=300, user_agent_extra=user_agent_extra)
104104

105-
# Control plane client for interpreter management
106-
self.control_plane_client = session.client(
107-
"bedrock-agentcore-control",
108-
region_name=region,
109-
endpoint_url=get_control_plane_endpoint(region),
110-
config=control_config,
111-
)
112-
113-
# Data plane client for session operations
114-
self.data_plane_client = session.client(
115-
"bedrock-agentcore",
116-
region_name=region,
117-
endpoint_url=get_data_plane_endpoint(region),
118-
config=data_config,
119-
)
105+
# Control plane client — let boto3 resolve endpoint natively (includes region validation).
106+
# Only pass endpoint_url when an environment override is set.
107+
cp_kwargs: dict = {"region_name": region, "config": control_config}
108+
if CP_ENDPOINT_OVERRIDE:
109+
cp_kwargs["endpoint_url"] = CP_ENDPOINT_OVERRIDE
110+
self.control_plane_client = session.client("bedrock-agentcore-control", **cp_kwargs)
111+
112+
# Data plane client — same pattern.
113+
dp_kwargs: dict = {"region_name": region, "config": data_config}
114+
if DP_ENDPOINT_OVERRIDE:
115+
dp_kwargs["endpoint_url"] = DP_ENDPOINT_OVERRIDE
116+
self.data_plane_client = session.client("bedrock-agentcore", **dp_kwargs)
120117

121118
self._identifier = None
122119
self._session_id = None

tests/bedrock_agentcore/services/test_identity.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ def test_initialization(self):
2929
mock_boto_client.assert_called_with(
3030
"bedrock-agentcore",
3131
region_name=region,
32-
endpoint_url="https://bedrock-agentcore.us-east-1.amazonaws.com",
3332
)
3433

3534
def test_create_oauth2_credential_provider(self):

tests/bedrock_agentcore/services/test_resource_policy.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ def test_initialization(self):
1717
mock_boto.assert_called_once_with(
1818
"bedrock-agentcore-control",
1919
region_name=TEST_REGION,
20-
endpoint_url=f"https://bedrock-agentcore-control.{TEST_REGION}.amazonaws.com",
2120
)
2221

2322
def test_put_serializes_dict_to_json(self):

0 commit comments

Comments
 (0)