Keep query-string tokens out of the uvicorn, nginx and Traefik access logs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ
This commit is contained in:
2026-10-06 17:32:51 +02:00
co-authored by Claude Opus 5.5
parent 1090bcabfd
commit 71b0f986e9
5 changed files with 73 additions and 0 deletions
+25
View File
@@ -4,6 +4,7 @@ import hashlib
import inspect
import ipaddress
import logging
import re
import sys
import tempfile
from collections.abc import AsyncIterator, Callable, Coroutine
@@ -58,6 +59,30 @@ 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]+")
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.
"""
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
for arg in record.args
)
return True
# The handshake lines go to `uvicorn.error`, the request lines to `uvicorn.access`.
for _name in ("uvicorn.access", "uvicorn.error"):
logging.getLogger(_name).addFilter(_RedactTokens())
def custom_generate_unique_id(route: APIRoute) -> str:
return f"{route.tags[0]}-{route.name}"
+28
View File
@@ -0,0 +1,28 @@
"""A token in a query string stays out of uvicorn's request lines."""
import logging
import fluksio.main # noqa: F401 - installs the filter
def test_a_token_in_the_query_is_not_logged() -> None:
records: list[logging.LogRecord] = []
handler = logging.Handler()
handler.emit = records.append # type: ignore[method-assign]
log = logging.getLogger("uvicorn.error")
level = log.level
log.addHandler(handler)
log.setLevel(logging.INFO)
try:
log.info(
'%s - "WebSocket %s" [accepted]',
"192.0.2.1:0",
"/api/v1/flows/ws?token=s3cr3t&x=1",
)
finally:
log.removeHandler(handler)
log.setLevel(level)
assert [record.getMessage() for record in records] == [
'192.0.2.1:0 - "WebSocket /api/v1/flows/ws?token=***&x=1" [accepted]'
]