From 87816ef6c98d3804ade28c24c90d49ef61dd5426 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 01:54:30 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20job=5Fid=20=D0=BD=D0=B5=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B4=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=B8=D1=88=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B2=20=D0=BB=D0=BE=D0=B3=20=D1=87=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=20repr=20(M-7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit job_id из сообщения, не прошедшего контракт, — данные отправителя; без экранирования перевод строки в нём позволял подделать строку лога. Теперь repr(job_id[:64]), как в рулинге R-18 для kid токена. Co-Authored-By: Claude Fable 5.1 --- api/src/services/recognition_exchange.py | 3 ++- api/tests/test_recognition_inbox.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/api/src/services/recognition_exchange.py b/api/src/services/recognition_exchange.py index a26e4c0..8ca7f2e 100644 --- a/api/src/services/recognition_exchange.py +++ b/api/src/services/recognition_exchange.py @@ -560,7 +560,8 @@ def _parse_message(msg: InboundMessage) -> RecognitionResultMessage | None: return RecognitionResultMessage.model_validate(data) except ValidationError as exc: raw_job_id = data.get("job_id") - job_id = raw_job_id[:LOG_ID_MAX_LENGTH] if isinstance(raw_job_id, str) else None + # job_id невалидного сообщения — данные отправителя: repr экранирует переводы строк и управляющие символы + job_id = repr(raw_job_id[:LOG_ID_MAX_LENGTH]) if isinstance(raw_job_id, str) else None errors = "; ".join( f"{'.'.join(str(part) for part in error['loc']) or ''}: {error['msg']}" for error in exc.errors(include_url=False, include_input=False, include_context=False) diff --git a/api/tests/test_recognition_inbox.py b/api/tests/test_recognition_inbox.py index 093e1df..e3d69c3 100644 --- a/api/tests/test_recognition_inbox.py +++ b/api/tests/test_recognition_inbox.py @@ -238,6 +238,18 @@ async def test_invalid_message_log_carries_job_id_when_readable(logs: list[Any]) assert str(_JOB) in record["message"] +async def test_invalid_message_job_id_is_escaped_in_log(logs: list[Any]): + """job_id невалидного сообщения с переводом строки попадает в лог через repr: подделать строку лога нельзя (M-7).""" + forged = "not-a-uuid\n2026-10-04 | INFO | подделанная запись\r\x1b[31m" + body = {**envelope(_JOB, _FILE, "done"), "job_id": forged} + msg = fake_msg(body, "result:x:1", validate=False) + await handle_message(msg, session_factory=_UntouchableFactory()) # type: ignore[arg-type] + (record,) = _logged(logs, "отклонено", "WARNING") + message = record["message"] + assert "\n" not in message and "\r" not in message and "\x1b" not in message + assert repr(forged) in message + + async def test_database_error_is_logged_and_not_raised(logs: list[Any]): """Ошибка БД (сервер недоступен) — `logger.exception`, без повторов, наружу не выходит.""" engine = create_async_engine("postgresql+asyncpg://nobody:nothing@127.0.0.1:1/none")