
Функция импорта удаленных изображений в Penpot позволяет аутентифицированному редактору файлов превратить обычную медиа-функцию в SSRF с серверной стороны, поскольку управляемые атакующим URL попадали в путь загрузки сервера, который следует за перенаправлениями, без фильтрации назначения.
Импорт удаленных изображений в Penpot позволял аутентифицированному редактору файлов превратить обычную функцию работы с медиа в SSRF из бэкенда, поскольку управляемые атакующим URL попадали в путь выборки с сервера, который следовал перенаправлениям без фильтрации целевых адресов.
Я нашел эту проблему при анализе Penpot, платформы с открытым исходным кодом для совместного дизайна и кода, задавшись очень конкретным вопросом:
Что происходит, когда инструмент совместного дизайна позволяет одному пользователю передать бэкенду URL удаленного изображения для загрузки?
В данном случае этот вопрос привел к реальной ошибке.
Поток импорта удаленных изображений Penpot принимал управляемый пользователем URL и заставлял бэкенд загружать его из серверного сетевого контекста без применения ограничений на целевые адреса loopback или частных сетей. Общий HTTP-клиент также автоматически следовал перенаправлениям.
Это превратило обычную функцию работы с медиа в примитив аутентифицированного SSRF из бэкенда, что в итоге привело к CVE-2026-45806.
Penpot: Penpot на GitHub
CVE: CVE-2026-45806
CVSS: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Это затронуло Penpot. На своем официальном сайте и в медиа-ките Penpot позиционирует себя как платформа с пользовательской базой, превышающей 1 млн человек, и сообщает, что десятки тысяч организаций используют ее, включая Blender, Mozilla, Fedora, NTT Data, MIT, Société Générale, Cisco, Fujitsu, Indra и ByteDance.
аутентифицированный редактор файлов -> управляемый атакующим URL удаленного изображения -> create-file-media-object-from-url -> загрузка изображения бэкендом fetch с включенными перенаправлениями -> итоговый запрос попадает на внутреннюю конечную точку, возвращающую изображение -> SSRF из бэкенда / достижимость внутренних ресурсов
Penpot — это платформа с открытым исходным кодом для совместного дизайна и кода.
Она обрабатывает такие вещи, как:
Это означает, что ее путь импорта медиа находится на реальной границе доверия.
Важный вопрос здесь заключался не в том, поддерживает ли Penpot импорт удаленных изображений.
Настоящий вопрос был таким:
Ограничивает ли Penpot, куда бэкенду разрешено подключаться, когда пользователь импортирует удаленное изображение?
В данном случае — нет.
Многие недооценивают функции удаленного импорта.
Это ошибка.
Как только приложение:
оно создает реальную границу доверия исходящих соединений.
В этом и была проблема.
Эта ошибка была не в рендеринге изображений. Не в хранении файлов. Не в обычных проверках разрешений на редактирование файла.
Это была классическая серверная ошибка доверия:
Этого достаточно, чтобы создать реальную уязвимость.
Я не подходил к Penpot, слепо фаззя случайные RPC-методы или ища сбои в первую очередь.
Более сильным подходом было определить наиболее перспективную границу безопасности.
Для Penpot это был удаленный импорт медиа.
Почему?
Потому что эта функция сочетает:
Это была правильная граница для проверки.
И именно здесь жила ошибка.
Ошибка сводится к короткой цепочке доверия.
Во фронтенде:
(defn upload-media-url
[name file-id url]
(rp/cmd!
:create-file-media-object-from-url
{:name name
:file-id file-id
:url url
:is-local true}))
управляемый пользователем url напрямую передается в RPC-вызов.
Затем в бэкенде:
(sv/defmethod ::create-file-media-object-from-url
...
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(files/check-edition-permissions! pool profile-id file-id)
...
(let [_ (files/get-minimal-file cfg file-id)
mobj (create-file-media-object-from-url cfg (assoc params :profile-id profile-id))])
и:
(defn- create-file-media-object-from-url
[cfg {:keys [url name] :as params}]
(let [content (media/download-image cfg url)
бэкенд проверяет, что вызывающий может редактировать целевой файл, а затем передает управляемый атакующим URL в media/download-image.
Реализация выборки находится здесь:
(defn download-image
"Download an image from the provided URI and return the media input object"
[{:keys [::http/client]} uri]
...
(http/req! client
{:method :get :uri uri}
{:response-type :input-stream})
А общий HTTP-клиент настроен так:
(http/build-client {:connect-timeout 30000
:follow-redirects :always}))
Вот и вся уязвимость:
Потому что атакующему нужно только:
Цепочка атаки проста:
Вот и вся ошибка.
Важное различие заключается в том, где выполняется запрос.
Вопрос не в том:
«Может ли Penpot импортировать изображения по URL?»
Реальный вопрос:
«Может ли аутентифицированный пользователь заставить бэкенд Penpot подключаться к внутренним адресам, к которым пользователь не должен иметь доступ через приложение?»
В данном случае ответ был «да».
Это важно, потому что есть реальная разница между:
Валидация изображений не устраняет эту разницу.
Она сужает некоторые случаи прямой эксфильтрации, но не устраняет условие SSRF или нарушение сетевой границы.
Я подтвердил эту проблему с помощью контролируемого локального доказательства, напрямую связанного с проверенным кодом Penpot.
Целью было не воздействие на стороннюю инфраструктуру. Целью было доказать точное свойство безопасности:
Я создал автономный валидатор на Java, который повторял соответствующее поведение:
content-type и content-lengthЯ проверил два случая.
Валидатор запросил: