Skip to content

Commit 0b8a342

Browse files
Route AsyncConnection unclosed-warning address through sanitize_for_log
Defence-in-depth parity with the sync sibling ``Connection._cleanup_loop_thread`` (connection.py:1140-1157), which routes the address through ``sanitize_for_log`` before the repr-escape. Python's ``repr()`` on ``str`` already escapes control codepoints into ``\xNN`` / ``\n`` / ``\r`` sequences (CWE-117 mitigated by repr alone), but the sanitiser is the package's documented belt-and-suspenders posture and the sync sibling applies it. The async sibling at aio/connection.py:60-117 was the lone asymmetric path. The behaviour change is cosmetic for clean addresses (no observable difference); for hostile / malformed addresses the control codepoints become literal ``?`` rather than the repr escape, matching the sync sibling's output shape. Do NOT add a sys.is_finalizing() short-circuit on the async sibling alone — the sync sibling uses module-level None checks for the same protection (and warnings.warn is already wrapped in contextlib.suppress(RuntimeError) at the warn site). A unified sys.is_finalizing() rollout would be a separate parity pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0e4add5 commit 0b8a342

2 files changed

Lines changed: 95 additions & 4 deletions

File tree

‎src/dqlitedbapi/aio/connection.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,19 @@ def _async_unclosed_warning(
107107
if closed_flag[0] or not connected_flag[0]:
108108
return
109109
with contextlib.suppress(RuntimeError):
110+
# Defence-in-depth parity with the sync sibling
111+
# ``_cleanup_loop_thread`` (connection.py:1140-1157):
112+
# route ``address`` through ``sanitize_for_log`` before
113+
# the repr-escape. Python's ``repr()`` already escapes
114+
# control codepoints into ``\xNN`` sequences (CWE-117
115+
# mitigated by repr alone), but the sanitiser is the
116+
# package's documented belt-and-suspenders posture and
117+
# the sync sibling applies it; this restores parity.
110118
warnings.warn(
111-
f"AsyncConnection(address={address!r}) was garbage-collected "
112-
f"without await close(). Call ``await aconn.close()`` "
113-
f"explicitly to avoid this warning and to release the "
114-
f"underlying socket promptly.",
119+
f"AsyncConnection(address={sanitize_for_log(str(address))!r}) was "
120+
f"garbage-collected without await close(). Call "
121+
f"``await aconn.close()`` explicitly to avoid this warning "
122+
f"and to release the underlying socket promptly.",
115123
ResourceWarning,
116124
stacklevel=2,
117125
)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Pin: ``AsyncConnection``'s GC-time ``ResourceWarning`` runs the
2+
caller-supplied ``address`` through ``sanitize_for_log`` before
3+
interpolating into the warning text, matching the sync sibling's
4+
``_cleanup_loop_thread`` "defence-in-depth" posture.
5+
6+
The sync ``Connection._cleanup_loop_thread`` (connection.py:1140-
7+
1157) sanitises via ``_sanitize_for_log(str(address))`` before
8+
``!r``; the async sibling ``_async_unclosed_warning`` used to
9+
interpolate the raw ``address`` directly. Python's ``repr()`` on
10+
``str`` already escapes LF / CR / U+2028 / NUL / ESC into ``\\xNN``
11+
sequences so the observable output is already log-injection-safe
12+
(CWE-117 mitigated by repr). The sanitize call adds an extra
13+
defence-in-depth layer (strips control codepoints to ``?`` before
14+
repr would escape them), which is the package's documented posture.
15+
16+
The fix is the narrow parity-restoration; no behaviour change on
17+
clean addresses, only a cosmetic difference for hostile / malformed
18+
addresses (which are theoretically unreachable through normal
19+
construction but documented as belt-and-suspenders).
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import os
25+
import warnings
26+
27+
from dqlitedbapi.aio.connection import _async_unclosed_warning
28+
29+
30+
def test_warning_routes_address_through_sanitize_for_log() -> None:
31+
"""An ``address`` containing control characters reaches the
32+
warning text via ``sanitize_for_log`` (matching the sync sibling
33+
at connection.py:1140-1157). ``sanitize_for_log`` replaces
34+
control codepoints with literal ``?`` BEFORE repr would escape
35+
them as ``\\xNN`` — the observable difference is that the
36+
sanitized text contains ``?`` where the raw text would have the
37+
repr escape sequence.
38+
39+
Python's ``repr()`` on ``str`` already escapes control chars
40+
into safe ``\\xNN`` / ``\\n`` / ``\\r`` sequences, so log
41+
injection (CWE-117) is already mitigated by repr alone. The
42+
sanitize call is the package's documented "defence-in-depth"
43+
layer; this test pins that the layer is present on the async
44+
surface (parity with the sync sibling)."""
45+
closed_flag = [False]
46+
connected_flag = [True]
47+
# Use a control character that ``sanitize_for_log`` strips to
48+
# ``?``. The literal ``\x01`` is a SOH control codepoint.
49+
malformed_address = "host:9001\x01TAG"
50+
51+
with warnings.catch_warnings(record=True) as captured:
52+
warnings.simplefilter("always")
53+
_async_unclosed_warning(closed_flag, connected_flag, malformed_address, os.getpid())
54+
55+
rw = [w for w in captured if issubclass(w.category, ResourceWarning)]
56+
assert len(rw) == 1
57+
msg = str(rw[0].message)
58+
# Without the sanitizer, the message would carry the repr escape
59+
# ``\x01TAG`` (5 chars after the colon segment); with the
60+
# sanitizer it carries ``?TAG``. Pin the post-fix shape.
61+
assert "?TAG" in msg
62+
# Negative: the repr escape MUST NOT appear (would prove the
63+
# sanitizer was bypassed). Look for the substring that repr
64+
# would emit for SOH: ``\x01``.
65+
assert "\\x01" not in msg
66+
67+
68+
def test_warning_preserves_clean_address_unchanged() -> None:
69+
"""Regression: a clean ``address`` reaches the warning text
70+
unchanged (modulo the standard repr escape). The sanitise call
71+
is a no-op for control-free strings."""
72+
closed_flag = [False]
73+
connected_flag = [True]
74+
clean_address = "10.0.0.1:9001"
75+
76+
with warnings.catch_warnings(record=True) as captured:
77+
warnings.simplefilter("always")
78+
_async_unclosed_warning(closed_flag, connected_flag, clean_address, os.getpid())
79+
80+
rw = [w for w in captured if issubclass(w.category, ResourceWarning)]
81+
assert len(rw) == 1
82+
msg = str(rw[0].message)
83+
assert "10.0.0.1:9001" in msg

0 commit comments

Comments
 (0)