# Модель угроз

## Периметр

**Контур доверенный: домашняя LAN, публичного интернета здесь нет — не
выдумывай его.** Сервис слушает `:8080` на хосте umbar внутри локальной сети,
наружу не проброшен, доменного имени и обратного прокси у него нет. Веб-UI и
REST API работают **без авторизации** осознанно; поле `[http].trusted_subnets`
зарезервировано, но не применяется.

Целевой периметр — **тот же**: выставлять jellybit в интернет не планируется.
Если это когда-нибудь изменится, первым шагом идёт задача «Авторизация веб-UI»,
и модель угроз пересматривается целиком, а не дополняется.

**Находки строятся против сегодняшнего периметра.** «Любой может открыть
страницу и удалить загрузку» — это принятое решение, а не дефект; чтобы стать
дефектом, ему нужен путь снаружи LAN.

## Недоверенный вход

Что приходит извне и каким каналом. Всё перечисленное контролируется не нами и
может быть враждебным по содержанию, даже когда канал доверенный.

| Что | Канал | Чем опасно |
| --- | --- | --- |
| Имена файлов и каталогов раздачи | qBittorrent API | разделители пути, `..`, управляющие символы, юникод-омоглифы, длина сверх лимита ФС |
| Имя торрента, поля magnet (`dn`, `tr`) | приём | то же плюс подстановка в промпт |
| Байты `.torrent` | приём (файл на форме) | bencode-разбор недоверенных данных, размер, вложенность |
| Текстовый контекст человека | все транспорты | попадает в промпт LLM целиком |
| Сообщение торрент-бота | Telegram (пересылка) | чужой формат, парсер, ссылки; текст автора бота, а не отправителя |
| **Ответ LLM** | HTTP к эндпоинту | целиком под влиянием входа выше; названия, годы, номера сезонов и серий, из которых строится целевой путь |
| Ответы метабаз | HTTP к TMDB/TVDB/TVMaze | канонические названия, из которых тоже строится путь; чистятся наравне с выходом LLM на каждой точке входа в план ([ADR-2026-08-10-sanitize-at-every-entry](adr/ADR-2026-08-10-sanitize-at-every-entry.md)) |
| Ответы qBittorrent | HTTP | пути, состояния, размеры |
| Запросы веб-UI и REST | LAN | идентификаторы, параметры действий |
| Пачка идентификаторов и признак подтверждения на групповом удалении | форма веб-UI | необратимое действие сразу по многим загрузкам; разбор, схлопывание дублей и предел размера стоят на **обеих** границах — подтверждении и исполнении, потому что вторая получает пачку формой заново |

**Выход LLM не отвечает за безопасность.** Инъекция в промпт считается
состоявшейся по умолчанию; защита стоит ниже — на валидации целевого пути.

## Из чего строятся пути и ключи

Отсюда строится выход за пределы песочницы — самое ценное место для враждебного
прохода.

- **Целевой путь** = `paths.movies`/`paths.series` + имя папки тайтла + (для
  сериала) `Season NN` + имя файла + расширение. Имя папки и файла собираются в
  `internal/naming` из полей распознавания: `title`, `original_title`, `year`,
  `season`, `episode`, provider-тег вида `[tmdbid-…]`. **Все эти поля —
  недоверенный вход.**
- **Правило:** компоненты санитизируются (убираются разделители пути, `..`,
  управляющие символы), финальный `filepath.Clean`-путь обязан быть **строго
  под** соответствующей библиотекой, иначе операция отклоняется. Проверка на
  результате, а не на входе. Следом — длина: каждый компонент обязан помещаться в
  255 байт UTF-8, иначе задача уходит в `review`. Порядок значим: путь, вышедший
  за песочницу, отклоняется как выход за библиотеку, а не как длинное имя, иначе
  находка безопасности спряталась бы за косметической причиной.
- **Исходный путь** = `save_path` из qBittorrent + относительное имя файла из
  `/torrents/files`. Тоже недоверенный, но по нему мы только **читаем и
  линкуем**; писать в `paths.downloads` нельзя вообще.
- **Ключ идентичности загрузки** — инфохэш (v1 SHA-1 / v2 SHA-256), нормализуется
  в lowercase hex фиксированной длины. Крафт-магнет с чужим или подставным
  хешем — известное направление атаки на владение (задача в беклоге).
- **Идентификаторы сущностей** — ULID, `ident.Parse` на каждой входной границе:
  строка из запроса не доходит до SQL непроверенной.
- **Владение целевым путём** — один путь, один владелец-`file_link`; смена
  владельца возможна только на свободном пути.

## Что разграничивает доступ

- **Telegram** — allowlist `telegram.allowed_user_ids`, **fail-closed**: пустой
  список запрещает всем. Это единственное реальное разграничение в системе.
- **Веб-UI и REST** — не разграничивают ничего: любой в LAN может всё.
  Осознанно, см. «Периметр».
- **Файловая система** — контейнер под `1000:1000`, смонтирована только
  песочница `/srv/media` и собственные каталоги `/config` (ro) и `/data`.
  `/srv/applications` целиком в контейнер не попадает.
- **qBittorrent** — логин и пароль WebUI; docker-подсеть намеренно не входит в
  его LAN-whitelist.

## Что чувствительнее чего

1. **Медиафайлы в раздаче** — единственное, что невосстановимо. Отсюда
   инвариант «источник неприкосновенен» и гард последней копии в Undo
   (`nlink <= 1` → отказ целиком).
2. **База `/data/jellybit.db`** — восстановима только перезапуском всей работы:
   теряется всё in-flight состояние и история привязок.
3. **Секреты**: пароль qBittorrent, ключи LLM и метабаз, токен Telegram,
   API-ключ Jellyfin. Живут только в `config.toml` (`0600`, рендерит деплой) и
   **никогда не попадают в логи, в диагностику состояния и в ответы API** —
   правило в [conventions/logging.md](conventions/logging.md).
4. **Библиотечные хардлинки** — восстановимы повторной раскладкой, поэтому
   ниже по шкале, хотя видны пользователю первыми.

## Что вне модели

Перечислено явно: против этого находки не строятся.

- **Злонамеренный участник LAN.** Сеть считается доверенной; «сосед по вайфаю
  удалил загрузку через веб-UI» — не дефект в сегодняшнем периметре.
- **Злонамеренный оператор.** Владелец может всё по определению, включая
  удаление раздачи вместе с файлами.
- **Компрометация соседних сервисов** — qBittorrent, Jellyfin, LLM-эндпоинта,
  хоста umbar. Если qBittorrent врёт про пути, мы проиграли раньше.
- **Отказ в обслуживании изнутри контура.** Огромная раздача, тысяча файлов,
  бесконечный ответ LLM — это вопросы устойчивости и ресурсов
  ([architecture.md](architecture.md) → «Эксплуатация»), а не безопасности.
  Тело ответа внешнего сервиса при этом читается с пределом: LLM — 8 MiB
  (`internal/llm`), метабазы — 4 MiB (`internal/metadata`).
- **Целостность содержимого медиафайлов.** Что в контейнере mkv — не наша забота.
- **Цепочка поставки** — модули Go, базовый образ distroless, плагины тулинга.
- **Приватность запросов к внешним сервисам.** Названия раздач уезжают в LLM и
  метабазы; это принято сознательно, прокси в конфиге есть.
- **Физический доступ к серверу и бекапам.**
