Skip to content

Commit d8ddff8

Browse files
oboehmerclaude
andcommitted
mask Authorization header in HTTP debug output when secrets are used
Prevent exposure of credentials in Robot Framework logs at DEBUG/TRACE levels by masking Authorization headers in HTTP connection debug output when Robot Secret types are detected. Changes: - Add check_and_process_secrets() to detect and process secrets in one pass - Track secret usage in sessions via _has_secrets attribute - Mask Authorization header in _print_debug() when secrets present - Import AUTHORIZATION constant from log module for consistency This ensures credentials are never logged even with debug=3, while still allowing Authorization headers to be visible for debugging when no secrets are used (e.g., test credentials). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent de8ebf6 commit d8ddff8

File tree

3 files changed

+84
-15
lines changed

3 files changed

+84
-15
lines changed

src/RequestsLibrary/RequestsKeywords.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
from RequestsLibrary import log
77
from RequestsLibrary.compat import urljoin
88
from RequestsLibrary.utils import (
9-
has_secrets,
9+
check_and_process_secrets,
1010
is_list_or_tuple,
1111
is_file_descriptor,
12-
process_secrets,
1312
warn_if_equal_symbol_in_url_session_less,
1413
)
1514

@@ -24,6 +23,7 @@ def __init__(self):
2423
self.timeout = None
2524
self.cookies = None
2625
self.last_response = None
26+
self._request_has_secrets = False
2727

2828
def _common_request(self, method, session, uri, **kwargs):
2929

@@ -32,12 +32,17 @@ def _common_request(self, method, session, uri, **kwargs):
3232
else:
3333
request_function = getattr(requests, "request")
3434

35-
# Process robot's Secret types included in auth
3635
auth = kwargs.get("auth")
37-
contains_secrets = False
3836
if auth is not None and isinstance(auth, (list, tuple)):
39-
contains_secrets = has_secrets(auth)
40-
kwargs["auth"] = process_secrets(auth)
37+
kwargs["auth"], contains_secrets = check_and_process_secrets(auth)
38+
else:
39+
contains_secrets = False
40+
41+
if session and hasattr(session, '_has_secrets'):
42+
contains_secrets = contains_secrets or session._has_secrets
43+
44+
# Store secrets flag for _print_debug to access
45+
self._request_has_secrets = contains_secrets
4146

4247
self._capture_output()
4348

src/RequestsLibrary/SessionKeywords.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import re
23
import sys
34

45
import requests
@@ -12,7 +13,8 @@
1213
from RequestsLibrary import utils
1314
from RequestsLibrary.compat import RetryAdapter, httplib
1415
from RequestsLibrary.exceptions import InvalidExpectedStatus, InvalidResponse
15-
from RequestsLibrary.utils import is_string_type, process_secrets
16+
from RequestsLibrary.log import AUTHORIZATION
17+
from RequestsLibrary.utils import is_string_type, check_and_process_secrets
1618

1719
from .RequestsKeywords import RequestsKeywords
1820

@@ -172,15 +174,20 @@ def create_session(
172174
Note that max_retries must be greater than 0.
173175
174176
"""
175-
auth = requests.auth.HTTPBasicAuth(*process_secrets(auth)) if auth else None
177+
# Check if auth contains secrets and process in one pass
178+
if auth:
179+
processed_auth, session_has_secrets = check_and_process_secrets(auth)
180+
auth = requests.auth.HTTPBasicAuth(*processed_auth)
181+
else:
182+
session_has_secrets = False
176183

177184
logger.info(
178185
"Creating Session using : alias=%s, url=%s, headers=%s, \
179186
cookies=%s, auth=%s, timeout=%s, proxies=%s, verify=%s, \
180187
debug=%s "
181188
% (alias, url, headers, cookies, auth, timeout, proxies, verify, debug)
182189
)
183-
return self._create_session(
190+
session = self._create_session(
184191
alias=alias,
185192
url=url,
186193
headers=headers,
@@ -196,6 +203,9 @@ def create_session(
196203
retry_status_list=retry_status_list,
197204
retry_method_list=retry_method_list,
198205
)
206+
# Store whether this session has secrets
207+
session._has_secrets = session_has_secrets
208+
return session
199209

200210
@keyword("Create Client Cert Session")
201211
def create_client_cert_session(
@@ -262,7 +272,12 @@ def create_client_cert_session(
262272
eg. set to [502, 503] to retry requests if those status are returned.
263273
Note that max_retries must be greater than 0.
264274
"""
265-
auth = requests.auth.HTTPBasicAuth(*process_secrets(auth)) if auth else None
275+
# Check if auth contains secrets and process in one pass
276+
if auth:
277+
processed_auth, session_has_secrets = check_and_process_secrets(auth)
278+
auth = requests.auth.HTTPBasicAuth(*processed_auth)
279+
else:
280+
session_has_secrets = False
266281

267282
logger.info(
268283
"Creating Session using : alias=%s, url=%s, headers=%s, \
@@ -300,6 +315,8 @@ def create_client_cert_session(
300315
)
301316

302317
session.cert = tuple(client_certs)
318+
# Store whether this session has secrets
319+
session._has_secrets = session_has_secrets
303320
return session
304321

305322
@keyword("Create Custom Session")
@@ -452,9 +469,15 @@ def create_digest_session(
452469
eg. set to [502, 503] to retry requests if those status are returned.
453470
Note that max_retries must be greater than 0.
454471
"""
455-
digest_auth = requests.auth.HTTPDigestAuth(*process_secrets(auth)) if auth else None
472+
# Check if auth contains secrets and process in one pass
473+
if auth:
474+
processed_auth, session_has_secrets = check_and_process_secrets(auth)
475+
digest_auth = requests.auth.HTTPDigestAuth(*processed_auth)
476+
else:
477+
digest_auth = None
478+
session_has_secrets = False
456479

457-
return self._create_session(
480+
session = self._create_session(
458481
alias=alias,
459482
url=url,
460483
headers=headers,
@@ -470,6 +493,9 @@ def create_digest_session(
470493
retry_status_list=retry_status_list,
471494
retry_method_list=retry_method_list,
472495
)
496+
# Store whether this session has secrets
497+
session._has_secrets = session_has_secrets
498+
return session
473499

474500
@keyword("Create Ntlm Session")
475501
def create_ntlm_session(
@@ -543,8 +569,9 @@ def create_ntlm_session(
543569
" - expected 3, got {}".format(len(auth))
544570
)
545571
else:
546-
auth = process_secrets(auth)
547-
ntlm_auth = HttpNtlmAuth("{}\\{}".format(auth[0], auth[1]), auth[2])
572+
# Check if auth contains secrets and process in one pass
573+
processed_auth, session_has_secrets = check_and_process_secrets(auth)
574+
ntlm_auth = HttpNtlmAuth("{}\\{}".format(processed_auth[0], processed_auth[1]), processed_auth[2])
548575
logger.info(
549576
"Creating NTLM Session using : alias=%s, url=%s, \
550577
headers=%s, cookies=%s, ntlm_auth=%s, timeout=%s, \
@@ -562,7 +589,7 @@ def create_ntlm_session(
562589
)
563590
)
564591

565-
return self._create_session(
592+
session = self._create_session(
566593
alias=alias,
567594
url=url,
568595
headers=headers,
@@ -578,6 +605,9 @@ def create_ntlm_session(
578605
retry_status_list=retry_status_list,
579606
retry_method_list=retry_method_list,
580607
)
608+
# Store whether this session has secrets
609+
session._has_secrets = session_has_secrets
610+
return session
581611

582612
@keyword("Session Exists")
583613
def session_exists(self, alias):
@@ -657,4 +687,14 @@ def _print_debug(self):
657687
debug_info = "\n".join(
658688
[ll.rstrip() for ll in debug_info.splitlines() if ll.strip()]
659689
)
690+
691+
# Mask Authorization header in debug output when secrets are used
692+
if self._request_has_secrets:
693+
debug_info = re.sub(
694+
rf'({AUTHORIZATION}:)\s*([^\n]+)',
695+
r'\1 *****',
696+
debug_info,
697+
flags=re.IGNORECASE
698+
)
699+
660700
logger.debug(debug_info)

src/RequestsLibrary/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,30 @@ def process_secrets(auth):
104104
return new_auth
105105

106106

107+
def check_and_process_secrets(auth):
108+
"""
109+
Check if auth contains secrets and process them in a single pass.
110+
111+
Returns:
112+
tuple: (processed_auth, has_secrets_flag)
113+
"""
114+
if not auth or not isinstance(auth, (list, tuple)):
115+
return auth, False
116+
117+
if robot_supports_secrets:
118+
has_secrets_flag = False
119+
processed = []
120+
for a in auth:
121+
if isinstance(a, Secret):
122+
has_secrets_flag = True
123+
processed.append(a.value)
124+
else:
125+
processed.append(a)
126+
return tuple(processed), has_secrets_flag
127+
else:
128+
return auth, False
129+
130+
107131
def utf8_urlencode(data):
108132
if is_string_type(data):
109133
return data.encode("utf-8")

0 commit comments

Comments
 (0)