From 3266c284a289f68e0993e35eef96ed66170ae013 Mon Sep 17 00:00:00 2001 From: Ghraven <115199279+Ghraven@users.noreply.github.com> Date: Wed, 6 May 2026 08:18:21 +0800 Subject: [PATCH] fix(_logs): redact sensitive headers that appear as formatted strings in log messages Fixes #1196 The existing `SensitiveHeadersFilter` only redacts headers when they are passed as a structured `dict` in `record.args`. However, when httpx logs at `DEBUG` level it interpolates headers directly into the message string (e.g. `"headers={'authorization': 'Bearer sk-...'}"`), bypassing the dict-path entirely and leaving the API key visible in plain text. Fix: add a second pass that inspects the fully-formatted log message for `header: value` patterns matching `SENSITIVE_HEADERS` and replaces the value with ``. When a substitution is made, `record.msg` is replaced with the sanitised string and `record.args` is reset so Python's logging machinery does not re-interpolate the original value. --- src/openai/_utils/_logs.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/openai/_utils/_logs.py b/src/openai/_utils/_logs.py index 376946933c..5e179fc8b4 100644 --- a/src/openai/_utils/_logs.py +++ b/src/openai/_utils/_logs.py @@ -34,9 +34,24 @@ def setup_logging() -> None: class SensitiveHeadersFilter(logging.Filter): @override def filter(self, record: logging.LogRecord) -> bool: + # Case 1: headers passed as a dict in record.args (structured logging) if is_dict(record.args) and "headers" in record.args and is_dict(record.args["headers"]): headers = record.args["headers"] = {**record.args["headers"]} for header in headers: if str(header).lower() in SENSITIVE_HEADERS: headers[header] = "" + + # Case 2: headers already interpolated into the log message string + # (e.g. httpx debug output: "headers={'authorization': 'Bearer sk-...'}") + import re + msg = record.getMessage() + for header in SENSITIVE_HEADERS: + # Match header: 'value' or header: "value" in the formatted message + pattern = rf"(?i)({re.escape(header)}['"]?\s*:\s*['"]?)([^'"\s,}}]+)" + redacted = re.sub(pattern, r"\1", msg) + if redacted != msg: + record.msg = redacted + record.args = () + break + return True