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")