Безопасность и приватность
LV24 SEO Optimizer работает локально. Движок (lv24-seo 0.5.2) выполняет работу с провайдерами, обход, хранение и отчёты. Приложение (0.2.2) — это UI и оболочка процессов вокруг движка; оно не переписывает провайдерские клиенты и не проксирует трафик.
Страница разделяет два продукта и два вида подписи:
- движок по умолчанию только читает и гейтит каждую запись;
- оболочка обеспечивает границу argv, путей и окружения;
- пакеты ОС по умолчанию не подписаны (Windows, Linux) или подписаны ad-hoc (macOS);
- пакеты обновлений подписаны minisign — это другая точка доверия.
Модель доверия
| Компонент | Уровень доверия | Какую границу обеспечивает |
|---|---|---|
| Движок | Доверенный: следит за семантикой провайдеров и write-гейтами | Работает от имени пользователя; пишет только в настроенные пути |
| Webview приложения | Недоверенный ввод | Фиксированная таблица команд Tauri, проверка аргументов |
| Rust-оболочка | Граница авторизации | Массивы argv, allow-list путей, фильтрация окружения |
| Ответ провайдера | Недоверенные данные | Разбор, ограничение размера, экранирование |
| Канал обновлений | Аутентификация встроенным ключом | HTTPS и подпись minisign |
Локальные данные остаются локальными: запросы, URL, трафик, снимки и отчёты лежат в настроенном хранилище. Сеть используется только для запрошенных вызовов провайдеров, обхода сайта, доставки по opt-in SMTP/Telegram и — в приложении — для запроса latest.json и пакета, который пользователь решил скачать. Телеметрии LV24 нет.
Вне области: скомпрометированная ОС или keychain, пользователь, который намеренно запускает вредоносный движок через LV24_SEO_BIN, и компрометация на стороне провайдера.
Секреты
Движок ищет секрет в таком порядке:
- переменные окружения
LV24_SEO_*; - необязательный OS keyring;
data/secrets.json, только если заданLV24_SEO_ALLOW_FILE_SECRETS=1.
Для поддержки OS keychain установите lv24-seo[secrets]. На POSIX файловый fallback создаётся с режимом 0600; Windows полагается на ACL каталога и файла, потому что POSIX-биты прав там не работают.
Флагов токена в командной строке нет. auth set читает скрытый ввод или stdin:
lv24-seo auth status --json
printf '%s' "$TOKEN" | lv24-seo auth set --provider gsc --stdinauth status показывает present, источник (env, keyring, file, missing) и срок действия, если он известен. Значение не выводится никогда. Токен из окружения доступен только для чтения: auth set и auth logout возвращают WRITE_DISABLED, пока он задан. Приложение показывает тот же источник, поясняет, что удаление файлового секрета не удаляет переменную окружения, и не предлагает действие удаления, которое не может сработать. Ответ auth logout с deleted: false показывается как «ничего не удалено», а не как успех.
Приложение и CLI используют одну и ту же запись в keychain для каждого провайдера: сервис lv24-seo, имя gsc_token, psi_api_key и т.д. Токен, введённый в терминале, уже доступен в auth status приложения, а токен из приложения виден CLI; второй копии в webview или каталоге данных приложения нет. Переменные LV24_SEO_* наследуются CLI-раннером приложения. Резервный <data-dir>/secrets.json общий только при одном workspace/data-каталоге.
Секретам не место в seo.toml, системе контроля версий, отчётах и тестовых фикстурах. Редактирующий форматтер маскирует чувствительные ключи, известные префиксы токенов и непрозрачные последовательности от 32 символов, но устанавливается только при заданном LV24_SEO_LOG_LEVEL. Callback CLI и lv24-seo mcp serve — единственные точки входа, которые его устанавливают; импорт библиотеки, create_server(), SeoService или клиент провайдера никогда не заменяют root-handlers вызывающего. Вывод — одна JSON-строка на запись, когда stderr не терминал (pipe, лог планировщика, MCP-хост), и обычная строка иначе. MCP-транспорт пишет только в stderr; stdout остаётся протоколом. Редактирование — защита в глубину; вызывающий код всё равно не должен логировать сырые учётные данные. config show маскирует похожие на секреты ключи как ***, и приложение не должно их «раскрывать».
Приложение не сохраняет секрет в webview. Ввод токена — это поле пароля; значение один раз уходит по IPC и сразу передаётся в lv24-seo auth set --provider <p> --stdin. Rust обнуляет буфер после завершения дочернего процесса. Значение не попадает в лог, MobX-хранилище, localStorage, буфер обмена, crash report или support bundle. На Linux CLI-раннер наследует переменные session bus и XDG, нужные Secret Service, а остальные переменные хоста фильтруются; само значение по-прежнему идёт только через stdin.
Только чтение по умолчанию
Чтения у провайдера безопасны без write-флага. Изменения у провайдера проходят через core/write_gate.py:
- План — dry run показывает цель, действие и полный хеш плана, затем возвращает короткоживущий
confirm_token. - Подтверждение — вызывающий проверяет план и повторяет запуск с
--allow-writeи этим токеном. - Заявка — хранилище занимает слот идемпотентности со статусом
pending. - Применение — вызов провайдера; успех переводит заявку в
sent, исключение провайдера освобождает её.
Токен — это HMAC над действием, целью, полным хешем плана и сроком действия; живёт он 300 секунд. Изменение URL, хоста, ключа, свойства или summary аннулирует токен. Write-вызовы никогда не повторяются автоматически. При инициализированной рабочей области CLI подключает SQLite-журнал идемпотентности, поэтому повторный токен возвращает status: skipped с reason: duplicate, и провайдер вызывается один раз. Без каталога данных применение всё равно выполняется, но конверт предупреждает, что защита от повтора отключена на пять минут жизни токена. Dry run не открывает журнал и не создаёт базу.
| Действие | Состояние в этой сборке |
|---|---|
| IndexNow submit | Подключён; нужны --allow-write, confirm-токен и LV24_SEO_WRITE_SECRET |
| GSC sitemap submit | WRITE_DISABLED — адаптер не подключён |
| Yandex recrawl | WRITE_DISABLED — адаптер не подключён |
| MCP write-инструменты | WRITE_DISABLED даже с allow_write=true; confirm-токена пока нет |
В этой сборке запись у провайдера умеет выполнять только CLI, и только IndexNow доходит до провайдера. MCP write-инструменты (seo_recrawl_request, seo_sitemap_submit) несут destructiveHint: true, но всё равно возвращают WRITE_DISABLED.
Инструменты локального состояния — другое дело. seo_crawl_site и seo_weekly_report сохраняют снимки обхода и недельного отчёта, поэтому помечены writes_local_state: true и readOnlyHint: false. Они не меняют состояние провайдера и не требуют confirm-токена.
Аннотации MCP
Аннотации описывают намерение, а не являются механизмом авторизации. Реестр выводит их из контракта инструмента:
| Инструмент | Тип | writes_local_state | readOnlyHint | destructiveHint |
|---|---|---|---|---|
seo_crawl_site | read | true | false | false |
seo_weekly_report | read | true | false | false |
seo_recrawl_request | write | false | false | true |
seo_sitemap_submit | write | false | false | true |
| Остальные семь | read | false | true | false |
Клиент, который смотрит на readOnlyHint, может запросить подтверждение перед обходом или недельным отчётом. Это намеренно: оба пишут локальное состояние. Клиент не должен считать read-аннотацию разрешением пропустить собственное подтверждение для гейтированной записи.
Сеть и защита от SSRF
Обходчик по умолчанию резолвит каждый запрошенный URL — seed и каждый редирект — и отказывается от частных, loopback, link-local, multicast, reserved и unspecified адресов, включая cloud metadata 169.254.169.254. Отказ — это типизированный результат blocked_private, а не исключение. Аудит локального сайта может разрешить частные адреса только из встраивания, которое задаёт allow_private; CLI-флага для этого нет.
Транспорт по умолчанию подключается к проверенному IP, сохраняя исходные HTTP Host и TLS SNI, поэтому при установке соединения имя хоста не резолвится повторно в другой адрес. Пользовательские внедрённые транспорты — доверенная граница тестов и встраивания, они должны сами обеспечивать политику адресов. Редиректы обрабатываются вручную, поэтому проверяется каждый переход, а понижение HTTPS→HTTP фиксируется как замечание о покрытии.
Оркестрация обхода проверяет robots и origin scope перед переходом. Сетевые ошибки robots и 5xx откладывают обход; robots — сигнал вежливости, а не контроль доступа. Ошибки HTTP провайдера содержат только хост и путь: query string может нести ключи, а заголовки — bearer-токены, поэтому ни то, ни другое не попадает в сообщение.
SMTP STARTTLS проверяет сертификаты до входа. Telegram отправляет простой текст и считает ошибки HTTP/Bot API неудачей. Доставка — at-least-once, не exactly-once: сбой после приёма на удалённой стороне, но до записи в ledger, может продублировать сообщение при повторе. HTTP rate limiting действует в пределах процесса и не гарантирует суточные квоты аккаунта между разными процессами CLI или MCP.
Данные и инъекции
SQLite хранится локально и игнорируется git. Относительный storage.data_dir из seo.toml привязывается к каталогу этого файла, поэтому один конфиг использует одну базу из любого рабочего каталога; встроенное значение по умолчанию и LV24_SEO_DATA_DIR остаются process-scoped. doctor --json сообщает meta.data_dir и meta.data_dir_source. Изменения схемы поставляются нумерованными миграциями и записываются в schema_migrations; обновление не удаляет пользовательские строки. Перед обновлением сделайте резервную копию базы. Старые строки с неизвестной временной гранулярностью сохраняются и помечаются, а не преобразуются молча.
SQL в репозиториях использует связанные параметры, без интерполяции строк. Многомерные ключи метрик кодируются в JSON, поэтому значения с | не конфликтуют. HTML отчёта экранирует каждое динамическое значение и не использует внешние изображения, стили, скрипты или tracking pixels. Приложение не встраивает HTML отчёта в webview: open_path проверяет абсолютный путь, требует его нахождения в разрешённом корне и применяет allow-list расширений, прежде чем передать файл обработчику ОС.
Граница оболочки приложения
Webview может вызывать только фиксированную таблицу команд с проверенными аргументами. Он не может передать исполняемый файл, shell-строку, сырую карту окружения или путь вне разрешённых корней. Все вызовы движка используют массив argv, абсолютный канонизированный --config, --json, ограниченный таймаут и cwd = config parent. Таймаут убивает конкретный дочерний handle или PID; wildcard-очистки процессов нет, потому что несколько приложений LV24 могут работать одновременно.
Окружение дочернего процесса фильтруется. Наследуются базовые имена платформы (PATH, SYSTEMROOT, HOME, APPDATA, переменные proxy и CA), переменные Linux Secret Service (DBUS_SESSION_BUS_ADDRESS, XDG_RUNTIME_DIR, XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME, TMPDIR) и все имена LV24_SEO_*. Остальные переменные хоста отбрасываются.
Запись конфигурации и настроек надёжна и приватна: патч toml_edit, временный файл 0600 рядом с целевым, fsync, валидация через lv24-seo config show, резервная копия 0600 с меткой времени (хранятся последние пять) и fsync, переименование, затем fsync родительского каталога и повторное чтение отредактированного эффективного конфига. Неудачная валидация оставляет исходное содержимое и mtime без изменений. Чтение ограничено 8 MiB и отклоняет нерегулярный или слишком большой файл. Настройки загружаются по полям, поэтому одно повреждённое значение не отбрасывает остальные. Устаревший сохранённый путь конфига допускается и показывается как отсутствующий. Зарезервированные имена Windows и завершающие точки/пробелы отклоняются в каждом компоненте пути. Сырого редактора нет; правки идут через валидируемые формы Settings и field-level патчи.
Состояние workspace, отчёта, doctor и секретов очищается при смене workspace или переходе движка в offline, поэтому данные предыдущего проекта не показываются в новом workspace. Действия, которые не могут выполниться, отключаются, а не падают по клику. Каталоги en/ru/zh согласованы; отсутствующий ключ — ошибка теста.
Production CSP webview не разрешает удалённые скрипты, стили, шрифты, изображения и connect-цели; он допускает собственные ресурсы, inline-стили, изображения asset:/data: и IPC. withGlobalTauri выключен, UI использует типизированные импорты API. В 0.2.2 нет локального HTTP API; будущему понадобятся собственная аутентификация и привязка к loopback.
Доверие к обновлениям
Пакеты обновлений используют бесплатный ключ Ed25519/minisign, отдельный от подписи кода ОС. Endpoint обновлений — это HTTPS-URL GitHub Releases, настроенный в src-tauri/tauri.conf.json; там же встроен публичный ключ. Приватный — секрет репозитория, который передаётся только на шаг сборки. Включён requireSignedVersion, поэтому объявленная версия привязана к подписанному пакету. HTTPS защищает транспорт; подпись — точка доверия.
Обновление принимает только версию выше текущей, поэтому откатиться нельзя. Оно никогда не скачивает и не устанавливает само: пользователь нажимает Download and install, а несохранённый черновик конфигурации блокирует кнопку. Ошибка подписи блокирует артефакт, помечает ошибку как non-retryable и ведёт на страницу релиза; она никогда не превращается в offline-предупреждение и не повторяется автоматически.
Непосредственно перед запуском установщика оболочка останавливает PID sidecar движка, зарегистрированные этим экземпляром приложения. Sidecar лежит внутри установленного каталога ресурсов, поэтому живой процесс держит его заблокированным и ломает NSIS-обновление. Останавливаются только точные отслеживаемые PID; ничего не сопоставляется по имени образа, поэтому другая установленная копия, portable-сборка или запуск в терминале не затрагиваются. Сбой очистки блокирует установку с UPDATE_ENGINE_BUSY, а не запускает установщик, который сломается на середине.
Автообновление работает только для Windows NSIS, macOS .app и Linux AppImage. deb, rpm, MSI и portable ZIP — manual-only: UI отключает встроенный установщик и ссылается на страницу релиза. У 0.1.0 нет updater, endpoint и публичного ключа, поэтому его пользователи один раз ставят 0.2.2 вручную.
Подпись пакета ОС — отдельная тема. По умолчанию сборки Windows не имеют подписи Authenticode, macOS подписаны только ad-hoc, пакеты Linux не подписаны. Подпись updater не делает пакет ОС доверенным для SmartScreen, Gatekeeper или политики дистрибутива.
Цепочка поставок
Зависимости закреплены package-lock.json, Cargo.lock и пином движка/PyInstaller в scripts/cli-checksums.json. Actions в CI движка закреплены по SHA; в workflow приложения actions закреплены по версии. У скачиваемых релизных инструментов есть контрольные суммы, сканирование секретов выполняется при ревью. Артефакты релиза получают контрольные суммы до публикации; SHA256SUMS.txt покрывает каждый опубликованный файл, включая подписи updater и latest.json.
Sidecar — это дерево PyInstaller onedir, а не onefile. scripts/verify-sidecar.mjs проверяет исполняемый файл и точную версию движка. Политика контрольных сумм учитывает пересборку: проверенная локальная сборка имеет строгий SHA-256-пин, а CI записывает фактический хеш своей невоспроизводимой пересборки в sidecar-checksums-<target>.json. Эти отчёты — доказательство для CI, а не релизные артефакты. Пин null означает «ещё не проверено», а не «проверено».
Релизный workflow ручной и требует владельца; триггеров push, pull-request или tag нет. Используются стандартные GitHub-hosted runners и только бесплатные минуты. Проект не покупает сертификаты подписи кода, notarization или платные runners.
Сообщение об уязвимости
Используйте приватный GitHub security advisory в затронутом репозитории. Не открывайте публичный issue с учётными данными, клиентскими запросами или деталями эксплуатации. Канонический контакт и политика поддерживаемых версий находятся в SECURITY.md каждого репозитория.
Укажите продукт (движок или приложение), точную версию, ОС, способ установки и минимальное воспроизведение. Удалите токены, cookies, заголовки Authorization, LV24_SEO_WRITE_SECRET, содержимое .env, data/secrets.json и JSON сервисного аккаунта. Секрет, который был раскрыт, нужно ротировать, а не просто удалить из последнего коммита.
См. Архитектура для границ доверия и Релизы и обновление для матрицы подписанных обновлений.
Source: _source/engine-security.md, _source/desktop-security.md, _source/engine-architecture.md, _source/desktop-architecture.md, _source/engine-releases-0.5.2.md, _source/desktop-releases-0.2.2.md.