Вышел Payload CMS 3.90: что изменилось в новой версии

19 сентября 2026 г.Александр Андреев

18 сентября вышли сразу два релиза Payload CMS: 3.90.0 и, вечером того же дня, 3.90.1. Актуальная версия — 3.90.1.

По номеру версии это обычный минор ветки 3.x и патч к нему, но пропускать связку нельзя: 3.90.0 — security-релиз с набором критических исправлений. Авторы прямо просят обновляться как можно скорее, даже если ни один из перечисленных пунктов проект не касается.

Подробностей эксплойтов, поверхности атаки и оценок severity в релиз-ноутах намеренно нет — CVE и GHSA-идентификаторы опубликуют отдельно. Разбираем то, что известно: изменения поведения, конфигурации и API, и что нужно сделать при обновлении.

После обновления — два обязательных шага

В этот раз апгрейд требует больше, чем поднять версии пакетов.

pnpm payload generate:types

pnpm payload migrate:create <migration-name>
pnpm payload migrate

Регенерация типов нужна всем. Миграция — проектам на реляционных СУБД: в user-коллекциях появилось новое поле resetPasswordRequestedAt, о нём ниже.

Пароли: смена отзывает сессии, сброс снимает блокировки

Два изменения аутентификации меняют поведение без всякой настройки.

Смена пароля теперь отзывает остальные сессии пользователя. Дополнительных действий не требуется — просто имейте в виду: после смены пароля другие открытые сессии разлогинятся.

Сброс пароля через forgot-password теперь очищает активные lockout-блокировки, а сам endpoint защищён троттлингом. За это отвечает новое поле resetPasswordRequestedAt в user-коллекциях — оно и требует регенерации типов и миграции на реляционных базах (в релиз-ноутах она названа add-reset-password-requested-at).

Scheduled publishing помнит, от чьего имени запланировано

Отложенные публикация и снятие с публикации теперь сохраняют auth-коллекцию пользователя, который запланировал событие.

Кого это касается: у кого на момент обновления стоят в очереди publish/unpublish события; кто использует несколько admin-capable auth-коллекций; кто ставит schedulePublish в очередь напрямую или зависит от сгенерированных типов этой задачи.

Что нужно сделать: пересоздать отложенные события после обновления, а в кастомном коде очереди передавать пользователя явно — user: { relationTo, value }, где value — id пользователя. Миграция базы не нужна, закоммиченные Payload-типы стоит перегенерировать.

Загрузки: самая большая группа изменений

Половина релиза — укрепление загрузок. Пробегусь по пунктам.

Строже валидация SVG и XML. Касается загрузки SVG, XHTML и XML-семейства, прямых клиентских загрузок (особенно Azure и кастомных интеграций) и кода, который полагается на сохранение path-компонентов в новых именах файлов. Нужно проверить затронутые workflow; если прежнее поведение нужно осознанно — allowRestrictedFileTypes: true в upload-конфиге коллекции.

Client uploads укреплены во всех адаптерах. Если используете clientUploads: true с S3, GCS, Azure или своим адаптером: для GCS в CORS allowed headers бакета нужно добавить x-goog-if-generation-match.

Azure-контейнеры по умолчанию приватные. @payloadcms/storage-azure с allowContainerCreate: true теперь создаёт приватные контейнеры. Прежнее публичное поведение возвращается опцией containerAccess: 'blob'.

disablePayloadAccessControl больше не отключает safe outbound fetch. Если в конфиге стоит disableAccessControl: true, безопасная загрузка внешних файлов всё равно работает. Для доверенных адресатов настраивается узкий allowlist upload.skipSafeFetch; полностью отключать проверку через skipSafeFetch: true стоит только если каждый URL, который принимает коллекция, заслуживает доверия.

Внешние файлы требуют доверенного origin. При upload.disableLocalStorage: true Payload теперь требователен к тому, откуда качает: нужен настроенный serverURL или origin приложения в CORS/CSRF; не-HTTP(S) URL внешних файлов больше не поддерживаются; externalFileHeaderFilter получил optional context, чтобы заголовки могли различаться по назначению:

upload: {
  externalFileHeaderFilter: (headers, context) => {
    // наружу — без пользовательских учётных заголовков
    if (!context?.isSameOrigin) {
      delete headers.cookie
      delete headers.authorization
    }
    return headers
  },
}

Имена файлов. Кастомное prefix-поле верхнего уровня, использовавшееся как обычные данные приложения, и смена storage-префикса без замены самого файла: первое нужно переименовать или обновлять только из доверенного серверного кода, второе — отправлять как часть замены файла.

Multipart-загрузки ограничены 50 МБ. Лимит по умолчанию — 50 МБ на весь raw multipart-запрос, включая файлы, поля и заголовки. Если нужно больше — поднимается через requestSizeLimit:

upload: {
  requestSizeLimit: 75 * 1024 * 1024, // пример: 75 MiB на весь multipart-запрос
}

Form Builder: заявки по умолчанию читает только админ-коллекция

Чтение form submissions по умолчанию ограничено админ-коллекцией. Это затрагивает проекты с несколькими auth-коллекциями, где второстепенным коллекциям нужно читать form-submissions или поле forms.emails.

Нужно явно задать formSubmissionOverrides.access или formOverrides.access. Уже настроенные оверрайды продолжают работать как работали.

Полиморфные joins: строгая валидация where

Полиморфные join'ы теперь применяют where-ограничения целиком и бросают QueryError, если фильтр не поддерживается.

Не поддерживаются:

  1. localized-поля;
  2. поля внутри arrays и blocks;
  3. пути через relationship, upload или JSON-поля — например, owner.email;
  4. операторы near, within, intersects и all;
  5. один и тот же путь поля с несовместимыми определениями в разных присоединяемых коллекциях — number в одной и text в другой.

Касается проектов, где join ссылается на несколько коллекций, а фильтр приходит через read access правило целевой коллекции, where-ограничение самого join или admin baseFilter/baseListFilter — в том числе при работе с folders.

Действие: проверить where-констрейнты и read access всех коллекций, задействованных в полиморфных join, и переписать неподдерживаемые фильтры на прямые совместимые поля, сохранив задуманные ограничения доступа.

API-ключи больше не видны после генерации

API-ключи после первичной генерации больше не читаются — ни через админку, ни внутри Payload-операций.

Прежнее поведение возвращается опцией reveal (по умолчанию false):

auth: {
  useAPIKey: {
    reveal: true,
  },
}

Изменение в духе проекта: ключ нужен в момент создания, а постоянно хранить его в читаемом виде — плохая практика.

Обновление Lexical — без миграции контента

В 3.90.0 вошёл bump версии Lexical. Для встроенного rich text это не требует ни изменений приложения, ни миграции данных.

Если поддерживаете кастомные rich text фичи — проверьте, что они компилируются и что кастомный контент корректно загружается, копируется и вставляется. Lexical удалил часть устаревших API, ужесточил TypeScript-типы и изменил загрузку и копирование кастомных нод; тесты, инспектирующие HTML редактора, могут потребовать обновлённых селекторов и снапшотов — Lexical добавляет служебную разметку.

Напоминание из документации: ставить lexical или @lexical/* для Payload самостоятельно не нужно — используйте ре-экспорты @payloadcms/richtext-lexical/lexical. Payload поставляет совместимые версии; смешивание версий может сломать редактор.

v3.90.1: патч в тот же день

Вечером того же дня вышел 3.90.1. Единственный багфикс: исправлена ошибка, при которой родительские where-условия ошибочно переносились во вложенные relationship-поля.

Остальное — chores: шаблоны подняты до Payload 3.90.0 и бэкпортировано обновление telemetry до v3.

Это чисто технический релиз, отдельной новости он не заслуживает — но обновляться нужно именно на него: dist-tag latest на npm уже указывает на 3.90.1.

Стоит ли обновляться?

Да, и не откладывая. 3.90.0 — редкий для ветки 3.x случай, когда релиз явно помечен как содержащий критические security-исправления, и авторы просят обновляться даже тех, кого изменения не касаются.

Перед обновлением мы бы проверили по проекту:

  • есть ли в очереди отложенные publish/unpublish события — после апгрейда их придётся пересоздать;
  • нет ли нескольких auth-коллекций и прямого queueing schedulePublish;
  • работу загрузок: SVG/XML, client uploads, внешние файлы, лимиты multipart;
  • полиморфные joins и folder-коллекции с base-фильтрами;
  • читают ли form submissions второстепенные auth-коллекции;
  • не зависит ли что-то от чтения API-ключей после генерации.

После обновления — pnpm payload generate:types, а на реляционных СУБД ещё и миграция для resetPasswordRequestedAt.

Полный changelog 3.90.0 и 3.90.1 опубликован на GitHub: github.com/payloadcms/payload/releases.

Назад в блог
Вышел Payload CMS 3.90: что изменилось в новой версии | Payload по-русски