From 1c3be98e677e4db1281ce849d70e01fc4712857b Mon Sep 17 00:00:00 2001 From: ZeliardM <140266236+ZeliardM@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:56:22 -0500 Subject: [PATCH] Keep the session token out of the HTTP client's debug log (#1835) Camera logins over `SslAesTransport` send their requests to `/stok=/ds`, and `HttpClient` logs every URL it posts to at debug, so the session token ends up in the debug logs people paste into issues. Co-authored-by: Claude Opus 5.5 --- kasa/httpclient.py | 7 ++++++- tests/test_httpclient.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/kasa/httpclient.py b/kasa/httpclient.py index 0a100d70..990b036b 100644 --- a/kasa/httpclient.py +++ b/kasa/httpclient.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio import builtins import logging +import re import ssl import time from typing import Any @@ -22,6 +23,9 @@ from .json import loads as json_loads _LOGGER = logging.getLogger(__name__) +# The session token in /stok=/ds URLs, e.g. after a camera's secure login +_SESSION_TOKEN = re.compile(r"(stok=)[^/?#]+") + def get_cookie_jar() -> aiohttp.CookieJar: """Return a new cookie jar with the correct options for device communication.""" @@ -86,7 +90,8 @@ class HttpClient: ) await asyncio.sleep(sleep) - _LOGGER.debug("Posting to %s", url) + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Posting to %s", _SESSION_TOKEN.sub(r"\1REDACTED", str(url))) response_data = None self._last_url = url self.client.cookie_jar.clear() diff --git a/tests/test_httpclient.py b/tests/test_httpclient.py index 8c3e743b..7143cded 100644 --- a/tests/test_httpclient.py +++ b/tests/test_httpclient.py @@ -1,4 +1,5 @@ import builtins +import logging import re import aiohttp @@ -114,3 +115,37 @@ async def test_httpclient_errors( assert mock_response.call_count == 1 else: assert conn.call_count == 1 + + +class _OkResponse: + status = 200 + connection = None + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_t, exc_v, exc_tb) -> None: + pass + + async def read(self) -> bytes: + return b"{}" + + +async def test_httpclient_debug_log_hides_session_token( + mocker: MockerFixture, caplog: pytest.LogCaptureFixture +) -> None: + """The /stok=/ds session token of a camera login is not logged.""" + + async def _post(url, *_, **__): + return _OkResponse() + + mocker.patch.object(aiohttp.ClientSession, "post", side_effect=_post) + client = HttpClient(DeviceConfig("127.0.0.1")) + + with caplog.at_level(logging.DEBUG, logger="kasa.httpclient"): + await client.post(URL("https://127.0.0.1/stok=S3cr3tT0k3n/ds"), data=b"x") + await client.post(URL("http://127.0.0.1/app/handshake1"), data=b"x") + + assert "S3cr3tT0k3n" not in caplog.text + assert "Posting to https://127.0.0.1/stok=REDACTED/ds" in caplog.text + assert "Posting to http://127.0.0.1/app/handshake1" in caplog.text