Skip to content

Commit 473bd20

Browse files
NguyenCong2kaclark4lifegithub-actions[bot]
authored
PYTHON-5928 Redact AWS session token from client repr (#2803)
Co-authored-by: Jeffrey 'Alex' Clark <aclark@aclark.net> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 81d8ab9 commit 473bd20

6 files changed

Lines changed: 103 additions & 0 deletions

File tree

doc/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Changes in Version 4.18.0
88
to the same server, avoiding a full handshake on each new connection.
99
Session resumption is supported on all Python versions for synchronous clients
1010
and on Python 3.11+ for async clients.
11+
- Redacted potentially sensitive authentication mechanism properties, including
12+
AWS session tokens, from the representations of
13+
:class:`~pymongo.synchronous.mongo_client.MongoClient` and
14+
:class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient`.
1115
- Command monitoring events and command log messages for a single logical
1216
operation now share one stable ``operation_id`` across all of its retry
1317
attempts, so consumers can correlate a retried operation's events. As a

pymongo/asynchronous/mongo_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,8 @@ def option_repr(option: str, value: Any) -> str:
13051305
return "document_class=dict"
13061306
else:
13071307
return f"document_class={value.__module__}.{value.__name__}"
1308+
if option == "authmechanismproperties":
1309+
value = common.redact_auth_mechanism_properties_for_repr(value)
13081310
if option in common.TIMEOUT_OPTIONS and value is not None:
13091311
return f"{option}={int(value * 1000)}"
13101312

pymongo/common.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,31 @@ def validate_read_preference_tags(name: str, value: Any) -> list[dict[str, str]]
423423
)
424424

425425

426+
_SAFE_AUTH_MECHANISM_PROPS_FOR_REPR = frozenset(
427+
[
428+
"ALLOWED_HOSTS",
429+
"CANONICALIZE_HOST_NAME",
430+
"ENVIRONMENT",
431+
"SERVICE_HOST",
432+
"SERVICE_NAME",
433+
"SERVICE_REALM",
434+
"TOKEN_RESOURCE",
435+
]
436+
)
437+
438+
439+
def redact_auth_mechanism_properties_for_repr(value: Any) -> Any:
440+
"""Redact sensitive auth mechanism properties before including them in repr."""
441+
if not isinstance(value, dict):
442+
return value
443+
444+
redacted = value.copy()
445+
for key in redacted:
446+
if str(key).upper() not in _SAFE_AUTH_MECHANISM_PROPS_FOR_REPR:
447+
redacted[key] = "<redacted>"
448+
return redacted
449+
450+
426451
def validate_auth_mechanism_properties(option: str, value: Any) -> dict[str, Union[bool, str]]:
427452
"""Validate authMechanismProperties."""
428453
props: dict[str, Any] = {}

pymongo/synchronous/mongo_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,8 @@ def option_repr(option: str, value: Any) -> str:
13061306
return "document_class=dict"
13071307
else:
13081308
return f"document_class={value.__module__}.{value.__name__}"
1309+
if option == "authmechanismproperties":
1310+
value = common.redact_auth_mechanism_properties_for_repr(value)
13091311
if option in common.TIMEOUT_OPTIONS and value is not None:
13101312
return f"{option}={int(value * 1000)}"
13111313

test/asynchronous/test_client.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,41 @@ def test_types(self):
196196

197197
self.assertRaises(ConfigurationError, AsyncMongoClient, [])
198198

199+
async def test_repr_redacts_aws_session_token(self):
200+
token = "SECRET_AWS_SESSION_TOKEN"
201+
client = AsyncMongoClient(
202+
"mongodb://AKIA:SECRET@localhost:27017/"
203+
f"?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:{token}",
204+
connect=False,
205+
)
206+
207+
the_repr = repr(client)
208+
209+
self.assertNotIn(token, the_repr)
210+
self.assertIn("'AWS_SESSION_TOKEN': '<redacted>'", the_repr)
211+
212+
async def test_repr_redacts_secret_auth_mechanism_properties(self):
213+
token = "SECRET_AWS_SESSION_TOKEN"
214+
api_key = "SECRET_API_KEY"
215+
client = AsyncMongoClient(
216+
"mongodb://AKIA:SECRET@localhost:27017/",
217+
authMechanism="MONGODB-AWS",
218+
authMechanismProperties={
219+
"aws_session_token": token,
220+
"CUSTOM_API_KEY": api_key,
221+
"TOKEN_RESOURCE": "mongodb://cluster.example",
222+
},
223+
connect=False,
224+
)
225+
226+
the_repr = repr(client)
227+
228+
self.assertNotIn(token, the_repr)
229+
self.assertNotIn(api_key, the_repr)
230+
self.assertIn("'aws_session_token': '<redacted>'", the_repr)
231+
self.assertIn("'CUSTOM_API_KEY': '<redacted>'", the_repr)
232+
self.assertIn("'TOKEN_RESOURCE': 'mongodb://cluster.example'", the_repr)
233+
199234
async def test_max_pool_size_zero(self):
200235
self.simple_client(maxPoolSize=0)
201236

test/test_client.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,41 @@ def test_types(self):
193193

194194
self.assertRaises(ConfigurationError, MongoClient, [])
195195

196+
def test_repr_redacts_aws_session_token(self):
197+
token = "SECRET_AWS_SESSION_TOKEN"
198+
client = MongoClient(
199+
"mongodb://AKIA:SECRET@localhost:27017/"
200+
f"?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:{token}",
201+
connect=False,
202+
)
203+
204+
the_repr = repr(client)
205+
206+
self.assertNotIn(token, the_repr)
207+
self.assertIn("'AWS_SESSION_TOKEN': '<redacted>'", the_repr)
208+
209+
def test_repr_redacts_secret_auth_mechanism_properties(self):
210+
token = "SECRET_AWS_SESSION_TOKEN"
211+
api_key = "SECRET_API_KEY"
212+
client = MongoClient(
213+
"mongodb://AKIA:SECRET@localhost:27017/",
214+
authMechanism="MONGODB-AWS",
215+
authMechanismProperties={
216+
"aws_session_token": token,
217+
"CUSTOM_API_KEY": api_key,
218+
"TOKEN_RESOURCE": "mongodb://cluster.example",
219+
},
220+
connect=False,
221+
)
222+
223+
the_repr = repr(client)
224+
225+
self.assertNotIn(token, the_repr)
226+
self.assertNotIn(api_key, the_repr)
227+
self.assertIn("'aws_session_token': '<redacted>'", the_repr)
228+
self.assertIn("'CUSTOM_API_KEY': '<redacted>'", the_repr)
229+
self.assertIn("'TOKEN_RESOURCE': 'mongodb://cluster.example'", the_repr)
230+
196231
def test_max_pool_size_zero(self):
197232
self.simple_client(maxPoolSize=0)
198233

0 commit comments

Comments
 (0)