Skip to content
KitploitKITPLOIT
ИнструментыЭксплойтыБлог
Log in
Отправить
ИнструментыЭксплойтыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2026-19264 — CVE-2026-19264 — Критическая неаутентифицированная уязвимость path traversal, приводящая к полному захвату экземпляра в Postiz (< 2.22.1). Технический разбор: обход порядка декодирования, эскалация через JWT_SECRET и анализ исправления в апстриме. | Kitploit
Инструменты/GitHubGitHub/darklycn1976/cve-2026-19264
Анализ уязвимостейАнализ КодаЭксплуатацияЭксплуатация веб-приложенийВеб-безопасностьОбучение и Образование
GitHubdarklycn1976/cve-2026-19264

CVE-2026-19264

CVE-2026-19264 — Критическая неаутентифицированная уязвимость path traversal, приводящая к полному захвату экземпляра в Postiz (< 2.22.1). Технический разбор: обход порядка декодирования, эскалация через JWT_SECRET и анализ исправления в апстриме.

РепозиторийСайт
81 месяц назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2026-19264 — неаутентифицированный обход пути с полным захватом экземпляра Postiz

CVE-2026-19264 — неаутентифицированный обход пути с полным захватом экземпляра Postiz

Автор: Krithik Babu P (@DarkLycn1976) Опубликовано: 2026-08-10 CVE: CVE-2026-19264 Серьёзность: Критическая — CVSS 4.0 9.3 / CVSS 3.1 9.8 CWE: CWE-22 — Некорректное ограничение пути в пределах ограниченного каталога Уязвимые версии: gitroomhq/postiz-app < 2.22.1 Исправлено в: v2.22.1


TL;DR

Postiz отдавал локально хранящиеся медиафайлы через маршрут, который склеивал сегменты пути из URL с каталогом загрузок и стримил результат обратно — без нормализации пути, проверки изоляции и аутентификации.

Очевидный traversal-пейлоад возвращает 404, потому что Next.js схлопывает сегменты ../ до маршрутизации. Но URL-закодированные разделители переживают сопоставление с маршрутом и декодируются ещё ровно один раз на пути к вызову файловой системы, восстанавливая обход уже после всех проверок.

Неаутентифицированный злоумышленник мог прочитать любой файл, доступный на чтение процессу приложения, — включая его собственное окружение, где хранится секрет подписи JWT. Поскольку Postiz подписывает токены сессий этим секретом и выдаёт их без срока действия (expiry claim), его получение превращает примитив чтения файлов в постоянную подделываемую сессию под любым пользователем, включая администратора.

Один неаутентифицированный GET-запрос — и полный захват экземпляра.


1. Предыстория

Postiz — это открытая платформа для планирования публикаций в социальных сетях — на момент написания примерно 34 000 звёзд на GitHub — построенная на фронтенде Next.js и бэкенде NestJS. Она широко используется для самостоятельного размещения (self-hosting) агентствами и небольшими командами для управления подключёнными аккаунтами соцсетей, запланированным контентом и биллингом.

Самостоятельно размещённые (self-hosted) инсталляции могут хранить загруженные медиафайлы локально, а не в объектном хранилище. Это поведение управляется одной переменной окружения:

STORAGE_PROVIDER=local

Именно это значение поставляется в .env.example, поэтому большинство self-hosted-инсталляций работают именно с ним, если только администраторы сознательно не настроили S3 или Cloudflare R2.

2. Поверхность атаки

Когда активно локальное хранилище, next.config.js переписывает публичный путь /uploads/:path* на внутренний API-маршрут:

apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts

Два свойства делают этот маршрут интересным ещё до рассмотрения какой-либо ошибки:

  1. Он неаутентифицирован. Никакое фронтенд-промежуточное ПО (middleware) его не защищает. Раздача публичных медиа — это задуманная функция, поэтому сессия не требуется.
  2. Это catch-all. Необязательный сегмент [[...path]] означает, что все оставшиеся компоненты пути приходят в виде массива, который обработчик волен интерпретировать.

Когда STORAGE_PROVIDER имеет любое значение, кроме local, перезапись указывает на /404, и обработчик недостижим. Этот конфигурационный переключатель — единственное, что отделяет инсталляцию от данной уязвимости.

3. Уязвимый код

Обработчик до версии v2.22.1:

export const GET = async (request: NextRequest, context) => {
  const { path } = await context.params;
  const filePath =
    process.env.UPLOAD_DIRECTORY + '/' + (path ?? []).join('/');

  const response  = createReadStream(filePath);
  const fileStats = statSync(filePath);
  // ... stream the file back to the caller
};

Три дефекта в четырёх строках:

  • Нет нормализации. path.normalize(), path.resolve() — не вызывается ни одна. Какие бы сегменты ни пришли, они склеиваются как есть.
  • Нет проверки изоляции. Ничто не проверяет, что итоговый filePath по-прежнему находится внутри UPLOAD_DIRECTORY.
  • Конкатенация строк, а не сборка пути. + '/' + обрабатывает компоненты как текст, а не как путь с семантикой.

Результат попадает напрямую в createReadStream(), и байты стримятся вызывающему с MIME-типом, определённым по имени файла. Нет ни списка разрешённых расширений, ни фильтра содержимого.

4. Почему очевидный пейлоад не работает

Атака из учебника выглядит так:

GET /uploads/../../../etc/passwd

На Postiz это возвращает 404, и именно этот 404 — вся причина, по которой эта ошибка так долго оставалась необнаруженной.

Next.js нормализует путь запроса во время маршрутизации. Сырые сегменты ../ схлопываются до того, как роутер решает, какой обработчик вызвать. К моменту, когда запрос достигает catch-all, обход уже устранён — либо путь резолвится в место без подходящего маршрута, либо обратно внутрь /uploads, но уже без точечных сегментов.

Для того, кто тестирует быстро, этот 404 читается как «фреймворк сам это обрабатывает». Это настоящая, работающая защита. Проблема не в том, что её нет, — а в том, на каком этапе конвейера она срабатывает.

5. Обход — расхождение в порядке декодирования

Сопоставление с маршрутом и обработчик запроса выполняют разное количество проходов процентного декодирования.

Если разделители процентно закодированы, во время сопоставления с маршрутом эта последовательность не является разделителем пути. %2e%2e%2f — просто непрозрачная строка, инертный текст, которого нормализатору незачем касаться. Она беспрепятственно проходит маршрутизацию, попадает под catch-all и декодируется по пути в params обработчика, где снова становится ../.

На этом этапе она конкатенируется с UPLOAD_DIRECTORY и передаётся в createReadStream() — в обход маршрутизации, в обход нормализации, в обход всех проверок, которые могли бы её остановить.

Рабочие варианты:

GET /uploads/%2e%2e%2fsecretdir%2fsecret.txt        → 200, file outside the upload directory
GET /uploads/..%2f..%2f..%2fetc%2fpasswd            → 200
GET /uploads/%2e%2e%2f%2e%2e%2f...%2fetc%2fpasswd   → 200, returned the real /etc/passwd

Двойное кодирование не работает — %252e остаётся литеральным на протяжении единственного прохода декодирования и никогда не становится точкой. Ровно один слой кодирования — это оптимальная точка, что служит полезным напоминанием: «закодируй сильнее» — не стратегия.

Инвариант, который стоит запомнить:

Проверка, выполняющаяся до завершения декодирования, не защищает sink.

6. Эскалация — от произвольного чтения к захвату экземпляра

Примитив чтения файлов сам по себе — это уровень High. Критическим его делает то, до чего он добирается.

Шаг 1 — читаем окружение. Собственная конфигурация Node-процесса лежит на диске в корне развёртывания. Из .env можно получить, среди прочего:

  • JWT_SECRET — ключ подписи токенов сессий
  • DATABASE_URL — полные учётные данные Postgres
  • OAuth-секреты подключённых провайдеров и ключи биллинга

Шаг 2 — подделываем сессию. Postiz подписывает токены сессий ключом JWT_SECRET алгоритмом HS256 через jsonwebtoken. Критически важно, что токены выдаются без expiresIn, поэтому подделанный токен действителен бессрочно.

Шаг 3 — становимся кем угодно. Аутентификационное промежуточное ПО заново извлекает пользователя из базы данных по claim'у id. Оно сознательно не доверяет таким claim'ам из токена, как isSuperAdmin, — хороший дизайн, — но это усиление защиты не имеет значения, если вы можете подписывать произвольный id. Подпись { id: <victim user id> } создаёт сессию, неотличимую от легитимного входа:

read .env  →  JWT_SECRET  →  sign({ id: victim })  →  authenticated as victim, forever
Скачать инструмент