Skip to content

Commit 52e2022

Browse files
authored
fix(aws-api-mcp-server): origin header parsing (#1851)
* Fix origin header parsing * Update CHANGELOG
1 parent b27ba88 commit 52e2022

File tree

3 files changed

+46
-26
lines changed

3 files changed

+46
-26
lines changed

src/aws-api-mcp-server/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- Origin header parsing (#1851)
13+
14+
## [1.1.7] - 2025-11-20
15+
16+
### Added
17+
18+
- Allow disabling local file system access (#1774)
19+
20+
### Fixed
21+
1222
- Deprecation warnings for passing transport settings when creating the server (#1772)
1323

24+
## [1.1.6] - 2025-11-19
25+
26+
### Changed
27+
28+
- Bump FastMCP to 2.13.1
29+
1430
## [1.1.5] - 2025-11-13
1531

1632
### Changed

src/aws-api-mcp-server/awslabs/aws_api_mcp_server/middleware/http_header_validation_middleware.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from fastmcp.server.dependencies import get_http_headers
1818
from fastmcp.server.middleware import Middleware, MiddlewareContext
1919
from loguru import logger
20+
from urllib.parse import urlparse
2021

2122

2223
class HTTPHeaderValidationMiddleware(Middleware):
@@ -41,7 +42,10 @@ async def on_request(
4142
raise ClientError(error_msg)
4243

4344
if origin := headers.get('origin'):
44-
origin = origin.split(':')[0] # Strip port if present
45+
# Strip port if present
46+
parsed_origin = urlparse(origin)
47+
origin = f'{parsed_origin.scheme}://{parsed_origin.hostname}'
48+
4549
allowed_origins = ALLOWED_ORIGINS.split(',')
4650

4751
if '*' not in allowed_origins and origin not in allowed_origins:

src/aws-api-mcp-server/tests/middleware/test_http_header_validation_middleware.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
@pytest.mark.parametrize(
1010
'origin_value,allowed_origins',
1111
[
12-
('example.com', 'example.com'), # Exact match
13-
('example.com:3000', 'example.com'), # With port
14-
('example.com', 'example.com,other.com'), # Multiple allowed origins
15-
('other.com', 'example.com,other.com'), # Second in list
16-
('example.com', '*'), # Wildcard
17-
('any-domain.com', '*'), # Wildcard allows any
12+
('http://example.com', 'http://example.com'), # Exact match
13+
('https://example.com:3000', 'https://example.com'), # With port
14+
('http://example.com', 'http://example.com,http://other.com'), # Multiple allowed origins
15+
('http://other.com', 'http://example.com,http://other.com'), # Second in list
16+
('https://example.com', '*'), # Wildcard
17+
('http://any-domain.com', '*'), # Wildcard allows any
1818
],
1919
)
2020
@patch('awslabs.aws_api_mcp_server.middleware.http_header_validation_middleware.get_http_headers')
@@ -43,9 +43,9 @@ async def test_origin_header_validation_passes(
4343
@pytest.mark.parametrize(
4444
'origin_value,allowed_origins',
4545
[
46-
('forbidden.com', 'example.com'), # Not in allowed list
47-
('forbidden.com', 'example.com,other.com'), # Not in multiple allowed
48-
('sub.example.com', 'example.com'), # Subdomain not matched
46+
('http://forbidden.com', 'http://example.com'), # Not in allowed list
47+
('http://forbidden.com', 'http://example.com,http://other.com'), # Not in multiple allowed
48+
('http://sub.example.com', 'http://example.com'), # Subdomain not matched
4949
],
5050
)
5151
@patch('awslabs.aws_api_mcp_server.middleware.http_header_validation_middleware.get_http_headers')
@@ -148,7 +148,7 @@ async def test_both_headers_validated_independently(mock_get_headers: MagicMock)
148148
"""Test that both host and origin headers are validated independently."""
149149
# Both headers present
150150
mock_get_headers.return_value = {
151-
'origin': 'example.com',
151+
'origin': 'http://example.com',
152152
'host': 'example.com',
153153
}
154154

@@ -159,7 +159,7 @@ async def test_both_headers_validated_independently(mock_get_headers: MagicMock)
159159
with (
160160
patch(
161161
'awslabs.aws_api_mcp_server.middleware.http_header_validation_middleware.ALLOWED_ORIGINS',
162-
'example.com',
162+
'http://example.com',
163163
),
164164
patch(
165165
'awslabs.aws_api_mcp_server.middleware.http_header_validation_middleware.ALLOWED_HOSTS',
@@ -178,7 +178,7 @@ async def test_host_fails_validation_when_both_present(mock_get_headers: MagicMo
178178
"""Test that host validation fails even when origin is valid."""
179179
# Both headers present, origin valid but host invalid
180180
mock_get_headers.return_value = {
181-
'origin': 'example.com',
181+
'origin': 'http://example.com',
182182
'host': 'malicious.com',
183183
}
184184

@@ -189,7 +189,7 @@ async def test_host_fails_validation_when_both_present(mock_get_headers: MagicMo
189189
with (
190190
patch(
191191
'awslabs.aws_api_mcp_server.middleware.http_header_validation_middleware.ALLOWED_ORIGINS',
192-
'example.com',
192+
'http://example.com',
193193
),
194194
patch(
195195
'awslabs.aws_api_mcp_server.middleware.http_header_validation_middleware.ALLOWED_HOSTS',
@@ -208,7 +208,7 @@ async def test_origin_fails_validation_when_both_present(mock_get_headers: Magic
208208
"""Test that origin validation fails even when host is valid."""
209209
# Both headers present, host valid but origin invalid
210210
mock_get_headers.return_value = {
211-
'origin': 'malicious.com',
211+
'origin': 'http://malicious.com',
212212
'host': 'example.com',
213213
}
214214

@@ -219,7 +219,7 @@ async def test_origin_fails_validation_when_both_present(mock_get_headers: Magic
219219
with (
220220
patch(
221221
'awslabs.aws_api_mcp_server.middleware.http_header_validation_middleware.ALLOWED_ORIGINS',
222-
'example.com',
222+
'http://example.com',
223223
),
224224
patch(
225225
'awslabs.aws_api_mcp_server.middleware.http_header_validation_middleware.ALLOWED_HOSTS',
@@ -248,23 +248,23 @@ async def test_no_origin_or_host_headers(mock_get_headers: MagicMock):
248248

249249

250250
@pytest.mark.parametrize(
251-
'origin_with_port,expected_hostname',
251+
'origin_with_port,expected_origin',
252252
[
253-
('example.com:3000', 'example.com'),
254-
('example.com:8080', 'example.com'),
255-
('localhost:5000', 'localhost'),
256-
('192.168.1.1:8000', '192.168.1.1'),
257-
('example.com', 'example.com'),
253+
('http://example.com:3000', 'http://example.com'),
254+
('https://example.com:8080', 'https://example.com'),
255+
('http://localhost:5000', 'http://localhost'),
256+
('http://192.168.1.1:8000', 'http://192.168.1.1'),
257+
('https://example.com', 'https://example.com'),
258258
],
259259
)
260260
@patch('awslabs.aws_api_mcp_server.middleware.http_header_validation_middleware.get_http_headers')
261261
@pytest.mark.asyncio
262262
async def test_port_removal_from_origin(
263263
mock_get_headers: MagicMock,
264264
origin_with_port: str,
265-
expected_hostname: str,
265+
expected_origin: str,
266266
):
267-
"""Test that port is correctly removed from origin/host before validation."""
267+
"""Test that port is correctly removed from origin before validation."""
268268
mock_get_headers.return_value = {'origin': origin_with_port}
269269

270270
middleware = HTTPHeaderValidationMiddleware()
@@ -273,7 +273,7 @@ async def test_port_removal_from_origin(
273273

274274
with patch(
275275
'awslabs.aws_api_mcp_server.middleware.http_header_validation_middleware.ALLOWED_ORIGINS',
276-
expected_hostname,
276+
expected_origin,
277277
):
278278
result = await middleware.on_request(context, call_next)
279279
assert result == 'success'
@@ -284,7 +284,7 @@ async def test_port_removal_from_origin(
284284
@pytest.mark.asyncio
285285
async def test_empty_allowed_origins(mock_get_headers: MagicMock):
286286
"""Test behavior when ALLOWED_ORIGINS is empty."""
287-
mock_get_headers.return_value = {'origin': 'example.com'}
287+
mock_get_headers.return_value = {'origin': 'http://example.com'}
288288

289289
middleware = HTTPHeaderValidationMiddleware()
290290
context = MagicMock()

0 commit comments

Comments
 (0)