Files
jellybit/docs/security.md
T
av 288be8ec34 web-ui: добавлена страница группового удаления загрузок
- выбор → поимённое подтверждение → отчёт: пачка до 20 загрузок, гарды входа
  на обеих границах, потолок времени и остановка после трёх подряд отказов
  внешнего сервиса
- допуск полного удаления сведён в единую точку store.State.CanDelete() —
  worker, страница загрузки и Telegram больше не держат своих перечней
2026-08-10 17:43:36 +03:00

11 KiB

Модель угроз

Периметр

Контур доверенный: домашняя 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)
Ответы 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.
  4. Библиотечные хардлинки — восстановимы повторной раскладкой, поэтому ниже по шкале, хотя видны пользователю первыми.

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

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

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