Skip to content

Commit e29ccea

Browse files
fix: redact proxy authentication in Socket Mode diagnostics
1 parent 48c82ca commit e29ccea

2 files changed

Lines changed: 90 additions & 3 deletions

File tree

‎slack_sdk/socket_mode/builtin/internals.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,11 @@ def _establish_new_socket_connection(
6060
ssl_context = _use_or_create_ssl_context(ssl_context)
6161

6262
if proxy is not None:
63-
parsed_proxy = urlparse(proxy)
64-
proxy_host, proxy_port = parsed_proxy.hostname, parsed_proxy.port or 80
63+
try:
64+
parsed_proxy = urlparse(proxy)
65+
proxy_host, proxy_port = parsed_proxy.hostname, parsed_proxy.port or 80
66+
except ValueError:
67+
raise ValueError("Invalid proxy URL") from None
6568
sock = socket.create_connection((proxy_host, proxy_port), receive_timeout)
6669
if hasattr(socket, "TCP_NODELAY"):
6770
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
@@ -80,7 +83,11 @@ def _establish_new_socket_connection(
8083
message.append("")
8184
req: str = "\r\n".join([line.lstrip() for line in message])
8285
if trace_enabled:
83-
logger.debug(f"Proxy connect request (session id: {session_id}):\n{req}")
86+
redacted_req = "\r\n".join(
87+
"Proxy-Authorization: ***" if line.partition(":")[0].strip().lower() == "proxy-authorization" else line
88+
for line in req.split("\r\n")
89+
)
90+
logger.debug(f"Proxy connect request (session id: {session_id}):\n{redacted_req}")
8491
with sock_send_lock:
8592
sock.send(req.encode("utf-8"))
8693
status, text = _parse_connect_response(sock)

‎tests/test_proxy_env_variable_loader.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import logging
22
import os
33
import unittest
4+
from base64 import b64encode
45
from unittest.mock import Mock, patch
56
from threading import Lock
67

78
from slack_sdk.proxy_env_variable_loader import load_http_proxy_from_env
9+
from slack_sdk.socket_mode.builtin.connection import Connection
810
from slack_sdk.socket_mode.builtin.internals import _establish_new_socket_connection
911
from tests.helpers import remove_os_env_temporarily, restore_os_env
1012

@@ -113,3 +115,81 @@ def test_socket_mode_proxy_error_redacts_credentials(self):
113115
self.assertNotIn("secret", str(error.exception))
114116
self.assertIn("http://***@proxy.example.com:3128", str(error.exception))
115117
self.assertIn("407", str(error.exception))
118+
119+
def test_socket_mode_proxy_trace_redacts_authorization(self):
120+
basic_credentials = b64encode(b"bob:secret").decode("ascii")
121+
for proxy, proxy_port, proxy_headers, authorization in (
122+
(
123+
"http://bob:secret@proxy.example.com:3128",
124+
3128,
125+
None,
126+
"Proxy-Authorization: Basic " + basic_credentials,
127+
),
128+
(
129+
"http://proxy.example.com",
130+
80,
131+
{"pRoXy-AuThOrIzAtIoN": "Bearer proxy-token", "X-Proxy-Request": "trace-test"},
132+
"pRoXy-AuThOrIzAtIoN: Bearer proxy-token",
133+
),
134+
):
135+
with self.subTest(proxy_headers=proxy_headers):
136+
logger = logging.getLogger("test_proxy_env_variable_loader")
137+
with patch("slack_sdk.socket_mode.builtin.internals.socket.create_connection") as connect, patch(
138+
"slack_sdk.socket_mode.builtin.internals._parse_connect_response",
139+
return_value=(200, "HTTP/1.0 200 Connection established"),
140+
), self.assertLogs(logger, level="DEBUG") as logs:
141+
_establish_new_socket_connection(
142+
session_id="test",
143+
server_hostname="example.com",
144+
server_port=443,
145+
logger=logger,
146+
sock_send_lock=Lock(),
147+
receive_timeout=1,
148+
proxy=proxy,
149+
proxy_headers=proxy_headers,
150+
trace_enabled=True,
151+
ssl_context=Mock(),
152+
)
153+
154+
expected_request = "CONNECT example.com:443 HTTP/1.0\r\n" + authorization + "\r\n"
155+
if proxy_headers is not None:
156+
expected_request += "X-Proxy-Request: trace-test\r\n"
157+
expected_request += "\r\n"
158+
connect.assert_called_once_with(("proxy.example.com", proxy_port), 1)
159+
connect.return_value.send.assert_called_once_with(expected_request.encode("utf-8"))
160+
161+
output = "\n".join(logs.output)
162+
for sensitive_value in ("bob", "secret", basic_credentials, "proxy-token"):
163+
self.assertNotIn(sensitive_value, output)
164+
self.assertIn("Proxy-Authorization: ***", output)
165+
self.assertIn("CONNECT example.com:443 HTTP/1.0", output)
166+
self.assertIn("HTTP/1.0 200 Connection established", output)
167+
if proxy_headers is not None:
168+
self.assertIn("X-Proxy-Request: trace-test", output)
169+
170+
def test_socket_mode_proxy_parsing_errors_do_not_log_credentials(self):
171+
secret = "sensitive-proxy-password"
172+
proxies = [f"http://bob:{secret}{delimiter}tail@proxy.example.com:3128" for delimiter in ("/", "#", "?")]
173+
for proxy in proxies:
174+
for trace_enabled in (False, True):
175+
with self.subTest(proxy=proxy, trace_enabled=trace_enabled):
176+
logger = logging.getLogger("test_proxy_env_variable_loader")
177+
on_error = Mock()
178+
connection = Connection(
179+
url="wss://example.com/socket-mode",
180+
logger=logger,
181+
proxy=proxy,
182+
trace_enabled=trace_enabled,
183+
on_error_listener=on_error,
184+
ssl_context=Mock(),
185+
)
186+
with patch("slack_sdk.socket_mode.builtin.internals.socket.create_connection") as connect:
187+
with self.assertLogs(logger, level="ERROR") as logs:
188+
connection.connect()
189+
connect.assert_not_called()
190+
on_error.assert_called_once()
191+
self.assertIsInstance(on_error.call_args[0][0], ValueError)
192+
output = "\n".join(logs.output)
193+
self.assertNotIn(secret, output)
194+
self.assertNotIn("bob", output)
195+
self.assertIn("Invalid proxy URL", output)

0 commit comments

Comments
 (0)