
Функция импорта удаленных изображений в 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, , , , и .

аутентифицированный редактор файлов -> управляемый атакующим 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Я проверил два случая.
Валидатор запросил:
http://127.0.0.1:7790/internal.png
Наблюдаемый результат:
http://127.0.0.1:7790/internal.pnghttp://127.0.0.1:7790/internal.png200image/pngЭто доказало, что логика выборки в стиле импорта напрямую приняла внутреннюю конечную точку, возвращающую изображение.
Затем валидатор запросил:
http://localhost:7791/redirect-to-internal
Эта конечная точка вернула HTTP-перенаправление на:
http://127.0.0.1:7790/internal.png
Наблюдаемый результат:
http://localhost:7791/redirect-to-internalhttp://127.0.0.1:7790/internal.png200image/pngВнутренний слушатель зафиксировал перенаправленный запрос.
Это доказало более важное утверждение:
Полезная нагрузка здесь была намеренно простой:
Это важно, потому что Penpot не просто загружает произвольные байты и останавливается. Он выполняет проверку, ориентированную на медиа, после запроса.
Поэтому правильным доказательством было не:
«бэкенд может попытаться куда-то подключиться»
Более сильным доказательством было:
«бэкенд можно заставить подключиться куда-то внутри и успешно завершить запрос при тех же ограничениях, связанных с изображениями, которые ожидает функция»
Именно это и продемонстрировала валидация.
Распространенная реакция на подобные ошибки SSRF:
«цель всё равно должна вернуть изображение»
Это наблюдение верно, но неполно.
Оно не устраняет уязвимость.
Оно лишь говорит вам, какие внутренние цели наиболее непосредственно полезны.
Эта проблема все еще позволяет:
Это все еще реальное нарушение границы безопасности.
Особенно в саморазмещенных средах внутренние сервисы часто существуют именно за этой границей.
В итоге этой проблеме был присвоен высокий уровень серьезности CVSS:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Эта классификация имеет смысл.
Утверждение не в том, что неаутентифицированный атакующий может мгновенно скомпрометировать каждое развертывание Penpot с нуля.
Утверждение в том, что любой обычный аутентифицированный редактор файлов может превратить Penpot в примитив бэкенд-запроса к внутренним адресам, включая доступ через перенаправления к loopback и целям в частных сетях.
Во время раскрытия были некоторые обсуждения серьезности, в основном вокруг:
Это разумные ограничения для обсуждения.
Но они не устраняют основную проблему:
Это реальная и обоснованная уязвимость SSRF.
Важное исправление здесь — не более строгая обработка MIME.
Настоящее исправление — политика исходящих адресов.
Корректное устранение для этого класса ошибок должно:
http и httpslocalhostЭто правильное направление исправления, потому что это была не ошибка парсинга изображений. Это была ошибка на границе доверия сети.
Эта проблема была сообщена конфиденциально через поток сообщений о безопасности GitHub.
Отчет включал:
Мейнтейнеры подтвердили проблему и начали работу над исправлением.
Позже проблеме был присвоен:
CVE-2026-45806
Ключевой урок здесь прост:
удаленный импорт медиа — это граница доверия исходящих соединений, а не просто функция удобства
Многие разработчики мыслят в терминах:
Это детали реализации.
Настоящий вопрос безопасности:
куда бэкенду разрешено подключаться от имени пользователя?
Если на этот вопрос нет явного ответа, такие функции, как удаленный импорт, по умолчанию становятся поверхностями SSRF.
Эта ошибка также подтверждает нечто важное в отношении анализа SSRF:
Вот настоящий вывод.
Эта уязвимость была не о броской полезной нагрузке.
Она была о том, чтобы задать правильный вопрос о границе доверия.
Penpot позволил аутентифицированному редактору файлов предоставить URL удаленного изображения, и бэкенд доверял этому URL больше, чем следовало. Обработка перенаправлений сделала остальное.
Вот почему это стало CVE-2026-45806.