Files
han-app/modules/module-05-message-safety.md
T

30 KiB
Raw Blame History

module-05. Проектная спецификация message-safety

Статус: целевая production-спецификация MVP.
Канонические источники: README.md, arch-00-glossary.md, arch-01-system-architecture.md, arch-02-api-contracts.md, arch-03-docker-compose-blueprint.md, arch-04-settings-and-content.md, arch-05-agent-development-process.md, module-01-api-backend.md.

1. Назначение и приоритет

message-safety — внутренний сервис, который до отправки сообщения в Bitrix24 проверяет пользовательский текст, содержащиеся в нём ссылки и файлы из S3-quarantine.

Сервис закрывает угрозы, поступающие через пользовательское сообщение:

  • управляющие и prompt-injection конструкции, направленные на оператора или последующую автоматическую обработку;
  • опасные URL-схемы, URL с credentials и ссылки на private/link-local/metadata адреса;
  • HTML/script-like payloads, способные стать активным содержимым при небезопасном отображении;
  • подмену типа файла, несоответствие заявленного MIME фактическому формату и checksum;
  • вредоносные файлы, обнаруживаемые антивирусными сигнатурами.

Спецификация детализирует архитектуру, но не меняет её. При конфликте приоритет имеют arch-00arch-05. Канонические domain outcomes:

  • 200 allow;
  • 403 deny;
  • 203 pending с последующим sticky 200 или 403.

Test-only правила по первому символу, случайные verdict и terminal 400 stub_final_error в production-контракт не входят.

2. Границы ответственности

2.1. Сервис отвечает за

  • строгую валидацию internal DTO;
  • нормализацию и rule-based проверку текста;
  • извлечение и проверку ссылок;
  • валидацию file metadata и фактического формата;
  • чтение файла из S3-quarantine по read-only credentials;
  • вычисление authoritative SHA-256;
  • антивирусную проверку файла через ClamAV;
  • выбор sync/async режима;
  • создание и исполнение async safety tasks;
  • sticky final verdict, verdict cache и audit в схеме message_safety;
  • task coordination/cache/rate limits в Redis DB2;
  • internal API, health, метрики, трассировку и безопасные JSON-логи.

2.2. Сервис не отвечает за

  • JWT пользователя, согласия и авторизацию доступа пользователя к диалогу;
  • edge/API rate limits;
  • загрузку файла и выдачу presigned URL;
  • запись пользовательского сообщения и статусов в han_app;
  • copy/promote файла из quarantine в S3-data и удаление объекта;
  • доставку в Bitrix24, realtime и пользовательский текст ошибки;
  • анализ входящих сообщений оператора;
  • ML-модерацию смысла, токсичности или правдивости текста.

Этими операциями владеет api-backend или соответствующий архитектурный модуль.

3. Threat model MVP

3.1. Текст и ссылки

Угроза Контроль Результат
Prompt/control injection Версионированные Unicode-aware rules 403 deny
Попытка выдать текст за system/developer instruction Нормализация + rule pack 403 deny
Script/active-content payload Правила для script, event-handler и опасных embedding-конструкций 403 deny
Опасная URL-схема Разрешены только http и https для распознанных web URL 403 deny
URL с userinfo/credentials Запрет user:password@host 403 deny
SSRF-ссылка DNS/IP classification, запрет private, loopback, link-local, multicast, unspecified и metadata endpoints 403 deny
Обход Unicode/whitespace NFKC, CRLF→LF, Unicode whitespace handling Проверка нормализованного текста
ReDoS/DoS правилами Линейные/ограниченные regex, лимиты текста, URL и времени 400 или dependency error

Rules не заменяют безопасный rendering. Frontend и Bitrix integration обязаны экранировать текст; safety является дополнительным барьером, а не HTML sanitizer.

3.2. Файлы

Угроза Контроль Результат
Недопустимый размер/MIME Сверка DTO с allow-list и лимитами 403 deny
Подмена MIME Magic-byte/content sniffing, сверка declared MIME 403 deny
Подмена содержимого после complete Полный SHA-256 против DTO checksum 403 deny
Malware ClamAV scan актуальными сигнатурами 403 deny
Архивная бомба/ресурсное истощение Лимиты размера, stream scan, ClamAV limits/timeouts deny при policy hit; error при сбое
Polyglot/неоднозначный формат Строгий формат detector и deny при mismatch/ambiguity 403 deny
Повтор известного файла Cache по SHA-256 + versions Sticky cached verdict

MVP принимает только типы из chat.attachments.allowed_extensions и chat.attachments.allowed_mime_types, при chat.attachments.max_size_mb. Расширение проверяет api-backend до вызова safety; message-safety независимо проверяет MIME и фактический формат байтов. Internal DTO не содержит имени файла, поэтому сервис не выводит расширение из object key.

3.3. Вне threat model MVP

  • zero-day malware, отсутствующий в сигнатурах и эвристиках выбранного AV;
  • OCR изображений и semantic analysis PDF;
  • password-protected/encrypted containers: в MVP они запрещаются, если содержимое нельзя полностью проверить;
  • DLP/поиск персональных данных, токсичности и запрещённой тематики;
  • переход по пользовательской ссылке и анализ удалённой страницы.

4. Общий pipeline

flowchart TD
  postCheck[POST_check] --> auth[Auth_and_DTO]
  auth --> kind{content_kind}
  kind -->|text| normalizeText[Normalize_text]
  normalizeText --> textRules[Text_rules]
  textRules --> linkRules[Link_pipeline]
  linkRules --> syncVerdict[200_or_403]
  kind -->|file| metadata[Metadata_validation]
  metadata --> cache{SHA256_cache}
  cache -->|hit| cached[Sticky_200_or_403]
  cache -->|miss| task[203_and_task]
  task --> worker[File_worker]
  worker --> objectRead[S3_stream_and_SHA256]
  objectRead --> formatCheck[Format_validation]
  formatCheck --> avScan[ClamAV_scan]
  avScan --> finalVerdict[Persist_sticky_verdict]
  finalVerdict --> taskGet[GET_task_200_or_403]

Приоритет:

  1. service authentication, body/content-type/size и DTO validation;
  2. idempotency/fingerprint conflict;
  3. нормализация;
  4. обязательные проверки для соответствующего content_kind;
  5. любой deny имеет приоритет над allow;
  6. инфраструктурная ошибка не превращается ни в allow, ни в domain deny.

5. Текстовый pipeline

5.1. Нормализация

Pipeline детерминирован:

  1. принять только JSON UTF-8;
  2. заменить CRLF/CR на LF;
  3. Unicode normalization NFKC;
  4. удалить leading Unicode whitespace;
  5. сохранить исходный регистр и punctuation для rules;
  6. ограничить текст internal DTO до 10 000 Unicode code points;
  7. вычислить SHA-256 нормализованного текста для correlation/cache без хранения текста.

content_kind=text требует поле text; пустой текст отклоняется upstream api-backend. content_kind=file допускает пустой text; текстовые rules тогда не запускаются.

5.2. Rule engine

Rules поставляются как статический read-only bundle приложения. Динамический код, regex или rule definitions из запроса запрещены.

Каждое правило содержит:

  • стабильный rule_id;
  • reason_code;
  • severity;
  • scope (text, url, file_metadata);
  • action (deny);
  • rules_version;
  • тестовые positive/negative cases.

Начальный rule pack MVP:

  • text.prompt_instruction_override — конструкции вида «игнорируй предыдущие инструкции» и эквиваленты на поддерживаемых языках;
  • text.prompt_role_impersonation — попытка обозначить пользовательский фрагмент как system/developer/tool instruction;
  • text.prompt_secret_extraction — запрос раскрыть system prompt, credentials, tokens или внутренние инструкции;
  • text.active_script<script>, inline event handlers и эквивалентные active-content шаблоны;
  • url.forbidden_scheme;
  • url.credentials_present;
  • url.private_destination.

Совпадение должно учитывать границы токенов и контекст, чтобы обычное обсуждение терминов не блокировалось простым substring match. Regex обязаны иметь ограниченную сложность и проходить ReDoS tests. Rule pack загружается и компилируется на startup; ошибка делает service not-ready.

Обычный текст без срабатывания обязательных rules получает 200 allow. Неуверенное отсутствие совпадения не является deny. Ошибка rule engine является 500, но не allow.

6. Pipeline ссылок

Сервис извлекает URL Unicode-aware parser-ом, а не одним regex.

Для каждой ссылки:

  1. ограничить количество ссылок в сообщении и длину URL;
  2. выполнить canonical parsing без автоматического исправления malformed URL;
  3. разрешить только http и https;
  4. запретить userinfo/credentials;
  5. нормализовать IDNA host и отклонить malformed/confusable host;
  6. для literal IP применить IP policy;
  7. для hostname выполнить DNS resolve через доверенный resolver и проверить все A/AAAA;
  8. запретить loopback, RFC1918/ULA, link-local, multicast, unspecified, reserved ranges и cloud metadata endpoints.

Сервис не загружает содержимое URL и не следует redirects. Поэтому URL scan не создаёт исходящий HTTP SSRF. DNS failure или malformed URL, явно распознанный как ссылка, возвращает deny по policy; недоступность resolver для всех ссылок является dependency error.

Лимиты URL должны быть техническими env/settings, документированными в arch-04 до реализации.

7. Файловый pipeline

7.1. Fast path POST

До создания задачи сервис:

  • валидирует attachment DTO;
  • проверяет допустимый MIME и size_bytes;
  • проверяет checksum sha256:<64 lowercase hex>;
  • проверяет, что quarantine_object_key соответствует разрешённому opaque key contract и не содержит traversal/control characters;
  • ищет cache по (sha256, scanner_engine, signatures_version, rules_version).

Явно недопустимые metadata дают sync 403 deny. Cache hit возвращает sticky 200 или 403. Cache miss создаёт задачу и возвращает 203 pending.

Сервис не доверяет key как path и не строит произвольный URL. S3 client обращается только к configured quarantine bucket с virtual-hosted addressing и read-only credentials.

7.2. Worker

Worker:

  1. атомарно claim-ит pending task с lease;
  2. открывает S3 object stream с ограничением bytes/time;
  3. вычисляет authoritative SHA-256;
  4. сверяет фактический размер и checksum с DTO;
  5. определяет реальный формат по содержимому;
  6. сверяет detector result с declared MIME;
  7. запрещает encrypted/password-protected и неподдерживаемые containers;
  8. передаёт поток в clamd через internal network;
  9. сохраняет sticky final verdict и audit;
  10. записывает cache только для terminal результата;
  11. завершает lease.

Чистый файл получает allow только если успешно завершились все обязательные проверки. ClamAV FOUND, checksum mismatch, format mismatch, unsupported encrypted content или policy limit дают deny с отдельным rule_id.

ClamAV timeout, protocol error, недоступность S3/DB/Redis или потеря lease не являются deny. Task остаётся pending/retryable в пределах deadline; после исчерпания retry получает terminal infrastructure failure, который API отдаёт как 503, а не 403.

7.3. AV runtime

MVP использует отдельный clamd sidecar/service в private Docker network:

  • порт не публикуется наружу;
  • сигнатуры обновляет freshclam;
  • readiness требует daemon PING и допустимый возраст signatures;
  • limits согласованы с максимальным размером файла;
  • контейнер non-root, read-only root filesystem где возможно, отдельный writable volume только для signatures/runtime;
  • worker не передаёт в clamd object key, имя пользователя или иные PII.

Недоступность AV переводит /health/ready в 503 и запрещает новые file allow.

8. Internal API

Все /internal/safety/v1/* доступны только api-backend в private network.

Headers:

X-Service-Token: ${MESSAGE_SAFETY_SERVICE_TOKEN}
X-Request-ID: UUID/ULID; при отсутствии генерируется сервисом
traceparent: optional W3C
Content-Type: application/json; charset=utf-8

Token сравнивается constant-time. Missing/invalid token → 401 service_unauthorized без подсказок.

8.1. POST /internal/safety/v1/messages/check

{
  "message_id": "uuid",
  "content_kind": "text",
  "text": "Текст сообщения",
  "attachment": null
}
{
  "message_id": "uuid",
  "content_kind": "file",
  "text": "",
  "attachment": {
    "attachment_id": "uuid",
    "quarantine_object_key": "opaque",
    "mime_type": "application/pdf",
    "size_bytes": 12345,
    "checksum": "sha256:<64-lowercase-hex>"
  }
}

Неизвестные поля запрещены. text и file — строгий discriminated union.

200:

{
  "verdict": "allow",
  "rule_id": "safety.all_checks_passed",
  "rules_version": "2026-01-01"
}

403:

{
  "verdict": "deny",
  "rule_id": "file.malware_detected",
  "reason_code": "message_blocked",
  "rules_version": "2026-01-01"
}

203:

{
  "verdict": "pending",
  "task_id": "uuid",
  "poll_after_ms": 2000,
  "expires_at": "2026-07-29T15:00:00Z",
  "rules_version": "2026-01-01"
}

8.2. GET /internal/safety/v1/messages/tasks/{task_id}

  • running/retryable → 203 pending;
  • sticky allow → 200 allow;
  • sticky deny → 403 deny;
  • malformed UUID → 400 validation_error;
  • unknown/expired → 404 task_not_found;
  • dependency failure → 503.

После final verdict повторный GET возвращает тот же HTTP status, verdict, rule_id и rules_version. Task не удаляется до retention expiry.

8.3. Error envelope

{
  "error": {
    "code": "validation_error",
    "message": "Request is invalid",
    "request_id": "uuid",
    "details": {}
  }
}
HTTP Code/смысл Retry
400 malformed DTO/path нет без исправления
401 service_unauthorized нет без исправления secret
403 domain deny нет
404 task_not_found dependency reconciliation
409 safety_request_conflict нет
429 rate_limit_exceeded по Retry-After
500 internal_error да
503 dependency/scanner/storage unavailable да

Domain 403 не считается circuit breaker failure.

9. Idempotency и state model

Fingerprint = SHA-256 canonical normalized DTO без token/request-id/trace headers.

  • одинаковый (message_id, fingerprint) возвращает тот же активный task или sticky final verdict;
  • тот же message_id с другим fingerprint → 409 safety_request_conflict;
  • concurrent duplicate создаёт одну task;
  • final verdict изменять запрещено;
  • transient error не кэшируется как allow/deny;
  • повторный worker delivery безопасен через task state compare-and-set.

Task states:

pending -> processing -> allowed
                      -> denied
processing -> pending (retry with lease expiry)
processing -> failed  (infrastructure retries exhausted)

allowed, denied, failed terminal и sticky. failed не является safety deny.

10. Хранение данных

10.1. PostgreSQL schema message_safety

safety_tasks:

  • id, message_id, attachment_id;
  • request fingerprint и content hash;
  • task status, attempts, lease owner/until, next attempt/deadline;
  • declared MIME/size/checksum;
  • verdict, rule_id, reason_code;
  • rules/scanner/signatures versions;
  • timestamps и common audit fields.

verdict_cache:

  • content SHA-256;
  • rules/scanner/signatures versions;
  • sticky verdict и rule;
  • expiry/created timestamps;
  • unique versioned cache key.

safety_audit:

  • request/task identifiers;
  • event (received, task_created, rule_hit, scan_completed, dependency_failed);
  • verdict/rule/version;
  • duration and technical error category;
  • timestamp.

Raw message text, file bytes, object key, filename, service token и AV stream в PostgreSQL не сохраняются. Для correlation используются UUID и cryptographic hashes.

10.2. Redis DB2

Redis используется для:

  • short-lived task lookup;
  • dedup/reservation coordination;
  • hot verdict cache;
  • leases/locks;
  • internal rate limits.

PostgreSQL остаётся durable source of truth. Потеря Redis не должна менять sticky final verdict; сервис восстанавливает state из PostgreSQL. Redis unavailable делает service not-ready и file task creation недоступным.

TTL task должен превышать MESSAGE_SAFETY_TASK_POLL_MAX_SEC плюс recovery/network margin. Verdict cache TTL задаётся отдельно и инвалидируется версиями rules/scanner/signatures.

11. Согласование с api-backend

POST check
200 -> allow -> text accepted or file promote -> Bitrix delivery
403 -> blocked/rejected -> no Bitrix -> public 422 message_blocked
203 -> persist han_app.safety_tasks -> poll GET
GET 203 -> continue
GET 200 -> allow branch
GET 403 -> deny branch
timeout/5xx -> public 503/504, no promote, no Bitrix

message-safety не перемещает и не удаляет S3 object. На deny это идемпотентно делает api-backend. Клиент не получает internal 203.

12. Security controls

  • internal network only; endpoint не публикуется через nginx и host port;
  • unique service token только из env/secret mount;
  • strict DTO, body/text/URL/file limits и запрет unknown fields;
  • read-only S3-quarantine access, без list/write/delete;
  • no arbitrary URL fetch, no redirect following;
  • egress allow-list только DNS, S3, PostgreSQL, Redis, OTLP и clamd по назначению;
  • parameterized SQL и least-privilege DB role только на schema message_safety;
  • Redis ACL только DB2 и prefixes han:safety:*;
  • dependency pinning, SBOM/image scanning;
  • non-root, read-only root fs, tmpfs /tmp, dropped capabilities, no-new-privileges;
  • OpenAPI UI выключен в production;
  • безопасные generic errors без stack/internal addresses;
  • правила и AV signatures обновляются только trusted deployment process.

13. Observability и privacy

JSON logs:

  • timestamp, level, service.name=message-safety, module, event;
  • request_id, trace_id, span_id, route, status, duration;
  • verdict, rule_id, rules/scanner/signatures version;
  • task state, attempt, age/size bucket;
  • dependency/error code.

Запрещено логировать message text, extracted URL целиком, file bytes, object key/name, checksum целиком, DTO body и service token. URL host и hash допустимы только при утверждённой retention policy; по умолчанию логируется category/hash.

Metrics:

  • request rate/latency/status по route;
  • checks/verdicts по content_kind, allow|deny|pending|error;
  • rule hits по low-cardinality rule_id;
  • task queue/age/attempts/lease conflicts/timeouts;
  • scan latency/bytes buckets/AV outcome;
  • signatures age/version info;
  • cache hit/miss;
  • PostgreSQL/Redis/S3/ClamAV latency and errors;
  • auth rejects/rate limit/readiness.

Telemetry collector unavailable не влияет на safety verdict и readiness.

14. Health

GET /health/live проверяет только process/event loop.

GET /health/ready проверяет:

  • settings, secret и rules bundle;
  • PostgreSQL read/write в schema message_safety;
  • Redis DB2 PING и prefixed SET/GET/DEL;
  • worker heartbeat/lease processing;
  • S3-quarantine Head/Get read permission на безопасный canary object;
  • ClamAV PING и допустимый возраст signatures;
  • OpenAPI schema availability.

Критическая dependency down → 503:

{
  "status": "not_ready",
  "components": {
    "postgres": "ok",
    "redis": "ok",
    "s3_quarantine": "ok",
    "worker": "ok",
    "antivirus": "down",
    "rules": "ok"
  }
}

Health не требует service token внутри private ops network и не раскрывает credentials/hostnames.

15. Configuration

Канонические существующие env:

APP_ENV=production-like
MESSAGE_SAFETY_DATABASE_URL=postgresql://message_safety_app:<secret>@<host>:5433/han_chat
MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/2
MESSAGE_SAFETY_SERVICE_TOKEN=<secret>
MESSAGE_SAFETY_RULES_VERSION=2026-01-01
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2
MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300
MESSAGE_SAFETY_FILE_SCAN_TIMEOUT_SEC=60
SELECTEL_S3_ENDPOINT_URL=https://s3.storage.selcloud.ru
SELECTEL_S3_BUCKET_QUARANTINE=han-chat-quarantine
SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY=<secret>
SELECTEL_S3_QUARANTINE_READ_SECRET_KEY=<secret>
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

ClamAV endpoint, signature max age, worker concurrency, retry/lease, task/cache TTL, text/URL limits и detector version требуют добавления в arch-04 до реализации. Бизнес allow-list типов/размера остаётся в app_settings; api-backend передаёт согласованные metadata, а safety использует versioned runtime snapshot.

Startup отклоняет placeholders, insecure production defaults, несовместимые timeout/TTL и отсутствующие обязательные параметры.

16. OpenAPI

message-safety/openapi.yaml OpenAPI 3.1 обязателен и содержит:

  • X-Service-Token security scheme;
  • common request/trace headers;
  • strict discriminated union text/file;
  • exact POST responses 200/203/400/401/403/409/429/500/503;
  • exact GET responses 200/203/400/401/403/404/429/500/503;
  • domain deny отдельно от error envelope;
  • UUID/checksum/max length examples;
  • health contracts.

Runtime schema сравнивается с committed artifact contract test. Terminal 400 stub_final_error отсутствует.

17. Тестовая матрица

Unit

  • NFKC, CRLF, Unicode whitespace, max length;
  • positive/negative cases каждого text rule;
  • границы токенов и false-positive corpus;
  • ReDoS/time budget всех regex;
  • URL parsing, IDNA, schemes, credentials и IP ranges IPv4/IPv6;
  • DTO union, fingerprint и rule priority;
  • MIME/magic/checksum decision table;
  • sticky state transitions;
  • log redaction.

Integration

  • PostgreSQL migration/constraints/idempotency/audit;
  • Redis DB2 reserve/cache/TTL/lease concurrency/recovery;
  • S3 read-only stream, object changed/missing/timeout;
  • ClamAV clean, EICAR, FOUND, timeout, daemon down, stale signatures;
  • full SHA-256 and size mismatch;
  • duplicate concurrent request and worker retry;
  • dependency recovery without changed final verdict.

Contract

  • text allow and each deny category;
  • file cache hit, 203 then sticky 200, 203 then sticky 403;
  • no random/non-sticky outcomes;
  • malformed 400 never treated as deny;
  • auth missing/wrong/correct;
  • request-id and trace propagation;
  • api-backend maps only domain 403 to public 422 message_blocked;
  • deny/timeout never calls Bitrix and never promotes quarantine;
  • OpenAPI runtime parity.

Security/failure

  • EICAR and safe corpus;
  • malformed/polyglot/encrypted files;
  • decompression and parser resource limits;
  • URL private/link-local/metadata/IPv4-mapped-IPv6 cases;
  • DNS rebinding simulation;
  • no raw text/token/URL/object key/checksum in logs/traces/errors;
  • S3 credentials cannot list/write/delete;
  • Redis ACL and PostgreSQL schema isolation;
  • telemetry outage does not affect verdict.

18. Definition of Done

  • production 200/203/403 contract реализован без stub divergence;
  • text rules и URL pipeline покрывают threat model и false-positive corpus;
  • files проходят metadata, authoritative SHA-256, format detector и ClamAV;
  • final task verdict sticky и durable;
  • idempotency/concurrency/recovery доказаны тестами;
  • PG schema message_safety, Redis DB2 и S3 read-only работают по least privilege;
  • cache versioned rules/scanner/signatures и не сохраняет transient errors;
  • api-backend mapping allow/deny/timeout проверен end-to-end;
  • blocked/failed content не попадает в Bitrix и не promote-ится;
  • health/readiness отражает PG/Redis/S3/workers/ClamAV/rules;
  • OpenAPI 3.1 и runtime parity зелёные;
  • logs/metrics/traces не содержат содержимое сообщений, файлов и secrets;
  • hardened containers запускаются без public port;
  • runbook описывает signature update, stale signatures, AV outage, retry и rollback.

19. Переход с текущей заглушки и follow-up

Текущая реализация в codebase/backend/message-safety/ остаётся test stub и не соответствует этой production-спецификации. Для перехода отдельной задачей необходимо:

  1. удалить правила ф/Ф, digit task и random RNG;
  2. удалить terminal 400 stub_final_error;
  3. реализовать sticky canonical 403 для async deny;
  4. подключить PostgreSQL schema message_safety, Redis DB2, S3 read-only и ClamAV workers;
  5. добавить migrations, полный OpenAPI и тестовую матрицу;
  6. синхронизировать module-01-api-backend.md и код api-backend, удалив test-only mapping stub_final_error;
  7. синхронизировать module-09-observability.md и dashboards, удалив stub distribution panels/маркировку;
  8. добавить новые technical env и ClamAV deployment contract в arch-04/arch-03 до production реализации.

До выполнения перехода контейнер должен быть явно маркирован как stub и не считаться production security control.