Fix attachment replay metadata sanitization
TerminallyLazy committed
Aug 10, 2026 at 21:06 UTC
e9d527406403e119d34a0ed20f902f3c0eb3607c
2 files changed
+61
-2
plugins/_a0_connector/api/ws_connector.py
+6
-2
@@ -3,7 +3,7 @@ from __future__ import annotations
3
4
import asyncio
5
from typing import TYPE_CHECKING, Any, ClassVar
6
-from urllib.parse import urlsplit
6
+from urllib.parse import unquote, urlsplit
7
8
from helpers.print_style import PrintStyle
9
from helpers.ws import WsHandler
@@ -82,8 +82,12 @@ def _attachment_log_metadata(attachments: list[str]) -> dict[str, list[str]]:
82
normalized = str(attachment or "").strip().replace("\\", "/")
83
if not normalized:
84
continue
85
- parsed = urlsplit(normalized)
85
+ try:
86
+ parsed = urlsplit(normalized)
87
+ except ValueError:
88
+ continue
89
path = parsed.path if parsed.scheme else normalized.split("?", 1)[0].split("#", 1)[0]
90
+ path = unquote(path).replace("\\", "/")
91
if path.endswith("/"):
92
continue
93
name = path.rstrip("/").rsplit("/", 1)[-1]
tests/test_a0_connector_attachment_metadata.py
+55
@@ -34,6 +34,15 @@ def test_attachment_log_metadata_omits_empty_metadata() -> None:
34
assert _attachment_log_metadata(["", "/"]) == {}
35
36
37
+def test_attachment_log_metadata_decodes_encoded_separators() -> None:
38
+ assert _attachment_log_metadata(
39
+ [
40
+ "https://host/%2Fhome%2Falice%2Fsecret.png",
41
+ "https://host/C:%5CUsers%5CAlice%5Csecret.png",
42
+ ]
43
+ ) == {"attachments": ["secret.png", "secret.png"]}
44
+
45
+
46
class RecordingLog:
47
def __init__(self) -> None:
48
self.calls: list[dict[str, object]] = []
@@ -139,6 +148,52 @@ async def test_websocket_text_only_message_keeps_empty_kvps(
148
assert log.calls[0]["kvps"] == {}
149
150
151
+@pytest.mark.asyncio
152
+async def test_websocket_malformed_attachment_url_keeps_message_delivery(
153
+ monkeypatch: pytest.MonkeyPatch,
154
+) -> None:
155
+ log = RecordingLog()
156
+ context = SimpleNamespace(log=log)
157
+ handler = WsConnector(None, None)
158
+ monkeypatch.setattr(
159
+ handler,
160
+ "_resolve_context",
161
+ AsyncMock(return_value=(context, "ctx-1")),
162
+ )
163
+ monkeypatch.setattr(
164
+ ws_module,
165
+ "subscribed_contexts_for_sid",
166
+ lambda sid: {"ctx-1"} if sid == "sid-cli" else set(),
167
+ )
168
+
169
+ scheduled: list[bool] = []
170
+
171
+ def close_scheduled(coroutine: object) -> SimpleNamespace:
172
+ getattr(coroutine, "close")()
173
+ scheduled.append(True)
174
+ return SimpleNamespace()
175
+
176
+ monkeypatch.setattr(asyncio, "create_task", close_scheduled)
177
+
178
+ result = await handler._handle_send_message(
179
+ {
180
+ "context_id": "ctx-1",
181
+ "message": "Review this",
182
+ "attachments": ["http://["],
183
+ "client_message_id": "client-malformed",
184
+ },
185
+ "sid-cli",
186
+ )
187
+
188
+ assert result == {
189
+ "context_id": "ctx-1",
190
+ "status": "accepted",
191
+ "client_message_id": "client-malformed",
192
+ }
193
+ assert scheduled == [True]
194
+ assert log.calls[0]["kvps"] == {}
195
+
196
+
197
@pytest.mark.asyncio
198
@pytest.mark.parametrize(
199
("payload", "code"),