From efd883d092ea3096f5344a48c6511a7e96371ad4 Mon Sep 17 00:00:00 2001 From: Melvin Strobl Date: Fri, 16 Oct 2026 21:57:35 +0200 Subject: [PATCH] Access log: the pairing poll's ?secret= and a socket's ?ticket= are masked too --- backend/fluksio/main.py | 10 ++++++---- backend/tests/test_access_log.py | 28 +++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/backend/fluksio/main.py b/backend/fluksio/main.py index 26fddf8..ff42b6f 100644 --- a/backend/fluksio/main.py +++ b/backend/fluksio/main.py @@ -62,21 +62,23 @@ from fluksio.flow.workers import PythonWorkerPool logger = logging.getLogger(__name__) -#: A credential in a query string: the live socket's and a worker's `?token=`. -_TOKEN_IN_QUERY = re.compile(r"([?&][\w-]*token=)[^&\s]+") +#: A credential in a query string: the live socket's `?token=` or `?ticket=`, a +#: worker's `?token=`, the pairing poll's `?secret=`. +_CREDENTIAL_IN_QUERY = re.compile(r"([?&](?:[\w-]*token|[\w-]*secret|ticket)=)[^&\s]+") class _RedactTokens(logging.Filter): """Keep query-string credentials out of uvicorn's request lines. uvicorn logs each request and websocket handshake with its query string, - and the live socket and a worker's attach carry their token there. + and the live socket, a worker's attach and a device polling for its + pairing carry their credential there. """ def filter(self, record: logging.LogRecord) -> bool: if isinstance(record.args, tuple): record.args = tuple( - _TOKEN_IN_QUERY.sub(r"\1***", arg) if isinstance(arg, str) else arg + _CREDENTIAL_IN_QUERY.sub(r"\1***", arg) if isinstance(arg, str) else arg for arg in record.args ) return True diff --git a/backend/tests/test_access_log.py b/backend/tests/test_access_log.py index fa830e2..257d268 100644 --- a/backend/tests/test_access_log.py +++ b/backend/tests/test_access_log.py @@ -1,4 +1,4 @@ -"""A token in a query string stays out of uvicorn's request lines.""" +"""A credential in a query string stays out of uvicorn's request lines.""" import logging @@ -26,3 +26,29 @@ def test_a_token_in_the_query_is_not_logged() -> None: assert [record.getMessage() for record in records] == [ '192.0.2.1:0 - "WebSocket /api/v1/flows/ws?token=***&x=1" [accepted]' ] + + +def test_the_pairing_secret_is_not_logged() -> None: + records: list[logging.LogRecord] = [] + handler = logging.Handler() + handler.emit = records.append # type: ignore[method-assign] + log = logging.getLogger("uvicorn.access") + level = log.level + log.addHandler(handler) + log.setLevel(logging.INFO) + try: + log.info( + '%s - "%s %s HTTP/%s" %d', + "192.0.2.1:0", + "GET", + "/api/v1/devices/pair/ABCD2345?secret=s3cr3t", + "1.1", + 202, + ) + finally: + log.removeHandler(handler) + log.setLevel(level) + + assert [record.getMessage() for record in records] == [ + '192.0.2.1:0 - "GET /api/v1/devices/pair/ABCD2345?secret=*** HTTP/1.1" 202' + ]