Skip to content

Operator API Security ru

Nemu-x edited this page Jul 7, 2026 · 1 revision

ClashFest Operator API — безопасность

🌐 English · Русский · 中文

Operator API — это trust-bounded поверхность расширения. Клиент трактует каждое значение как недоверенный вход из сети и применяет жёсткую валидацию, прежде чем оно коснётся UI, темы или диска.

Этот документ описывает, что клиент гарантирует, что нет, и от каких атак мы защищаемся.

Threat model

Релевантные классы атакующих:

  1. Злонамеренный оператор — контролирует ответ URL подписки. Может слать враждебные заголовки (oversize значения, пейлоады, вредоносные URL). Мы относимся к нему как к любому внешнему входу.
  2. Сетевой атакующий — перехватывает HTTP-ответ подписки в скомпрометированной сети (редко, т.к. подписки по HTTPS).
  3. Компрометация image-хоста оператора — CDN лого захвачен и начинает отдавать враждебные изображения / редиректы во внутренние сервисы.
  4. App-store ревьюер / регулятор — должен видеть, что «брендинг» не может тихо превратить приложение в другое (смена идентичности ограничена тем, что видит юзер, а не тем, как приложение себя ведёт).

Заметка: оператор, который к тому же провайдер подписки, и так может абьюзить многое вне этого API (выбор прокси, правила роутинга). Брендинг добавляет пару ручек — они должны быть безопасны по отдельности.

Что клиент валидирует

Строки

  • Trim пробелов
  • Удаление control-символов (U+0000U+001F, U+007F, кроме \n / \r / \t где поле разрешает)
  • Truncation до макс. длины поля (см. Справочник заголовков)
  • Декодирование base64: префикса
  • После всего — blank → поле считается отсутствующим

URL

Принимаются, только если начинаются с одного из:

  • https://
  • tg://
  • mailto:
  • t.me/ (auto-промоут в https://t.me/)

http:// отклоняется для брендинг-URL. Единственное исключение — метадата-пробы самих URL подписки, где юзер явно согласился на plaintext подписку.

URL дополнительно проходят looksLikeUrl (длина, без встроенных пробелов).

Hex-цвета

  • Только regex ^#[0-9A-Fa-f]{6}$ — никаких #RGB, rgba(), именованных.
  • Клиент считает WCAG-яркость против цвета поверхности. Если контраст ниже 3:1 (минимум AA «large text») — цвет отклоняется, используется дефолтный акцент.

Булевы / Enum / Integer

  • Boolean: true/1/yes/on → true; false/0/no/off → false; иначе — заголовок отсутствует.
  • Enum: только документированные значения; неизвестные игнорируются.
  • Subscription-Userinfo квоты: Long ≥0; expire= в [0, now + 10 лет], иначе игнор. X-Brand-Max-Devices: 1–999.

Изображения (X-Brand-Logo-URL, X-Brand-Logo-Light-URL)

Поле наивысшего риска — качаем произвольные байты по выбранному оператором URL. Защиты:

Транспорт

  • Только HTTPS. http:// отклоняется до построения запроса.
  • Никаких редиректов в приватные сети. Перед каждым хопом резолвнутый IP проверяется против:
    • 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16
    • ::1/128, fc00::/7, fe80::/10
    • Любого другого RFC1918 / RFC4193 / RFC6890 зарезервированного пространства
  • Connect + read таймауты: 15с каждый · Макс. редиректов: 3
  • Привязано к DNS-резолверу устройства (без системного оверрайда во время фетча)

Ответ

  • Content-Type должен быть image/png, image/webp или image/jpeg. Всё прочее (включая image/svg+xml, image/gif, text/html) отклоняется.
  • Content-Length (если есть) ≤ 512KB. Если отсутствует — чтение капается на 512KB, любой байт сверх прерывает загрузку.
  • Байты парсятся BitmapFactory.decodeByteArray сначала в bounds-only режиме; второй декод только если outWidth × outHeight × 4 ≤ 4MB (макс ~1024×1024).
  • Анимированные (animated WebP) декодятся как первый кадр.

Хранилище

  • Лого в <filesDir>/brand/<sha256(url)> (приватно приложению)
  • Атомарная запись: tmp + rename
  • Чистится: при удалении подписки; при новом лого с другим URL для той же подписки
  • Никогда не пишется во внешнее хранилище, недоступно другим приложениям

Почему не SVG

SVG может нести XSS-подобные пейлоады (<script>, xlink:href на удалённые ресурсы, CSS-импорты, foreignObject HTML). Даже безопасный парсинг не гарантия против DoS (billion-laughs, geometry explosion). Лого — это флаг-битмапы; PNG/WebP покрывают кейс без SVG-attack surface.

Что клиент не валидирует

  • Правда ли лого представляет оператора? Нет — мы не верифицируем владение брендом.
  • Правда ли сайт оператора его? То же.
  • Совпадает ли имя бренда с панелью? Нет — свободная форма.

Это операторские trust-вопросы, не клиент-безопасность. Юзер уже выбрал доверять этой подписке, добавив её.

Смена идентичности видима

By design, брендинг никогда тихо не меняет идентичность приложения так, чтобы юзер не заметил:

  • Иконка лаунчера и имя пакета Android никогда не меняются. Ревьюеры стора всегда опознают приложение.
  • Экран About всегда показывает строку <brand> — powered by ClashFest (непереводимую, фиксированную).
  • Когда брендинг активен, в Settings есть «Reset branding» — восстанавливает дефолтную идентичность в один тап.

Когда брендинг сбрасывается

Кэш бренда (имя, лого, акцент, ссылки) чистится:

  • Активный профиль удалён (и других профилей нет)
  • Юзер нажал «Reset branding» в Settings
  • Последующий фетч той же подписки вернул заголовки без brand-значения (очистка брендинга — операторское действие, не залипание навсегда)

Мы не сбрасываем брендинг на апгрейде приложения — он переживает бампы версий.

Rate / retry

  • Фетчи лого — не чаще раза за цикл обновления подписки
  • Неудачные фетчи запоминаются на 1ч, чтобы избежать retry-штормов
  • «Активный бренд» в SharedPreferences — источник истины между обновлениями: потеря сети не стирает брендинг посреди сессии

Репортинг

Если нашли security-проблему в Operator API или парсере — следуйте security-политике в корне репозитория.

Clone this wiki locally