
L'importation d'images à distance de Penpot permet à un éditeur de fichiers authentifié de transformer une fonctionnalité de commodité multimédia normale en SSRF d'origine serveur, car des URL contrôlées par l'attaquant ont traversé un chemin de récupération de serveur suivant des redirections sans filtrage de destination.
La fonction d'importation d'images distantes de Penpot permettait à un éditeur de fichier authentifié de transformer une fonctionnalité multimédia normale en SSRF d'origine serveur, car des URL contrôlées par l'attaquant tombaient dans un chemin de récupération serveur suivant les redirections, sans filtrage de destination.
J'ai découvert ce problème en examinant Penpot, la plateforme open-source de conception et de collaboration sur le code, avec une question très spécifique en tête :
Que se passe-t-il lorsqu'un outil de conception collaborative permet à un utilisateur de fournir au backend une URL d'image distante à récupérer ?
Dans ce cas, cette question a conduit à un véritable bug.
Le flux d'importation d'images distantes de Penpot acceptait une URL contrôlée par l'utilisateur et faisait récupérer cette URL par le backend depuis le contexte réseau du serveur, sans imposer de restrictions de destination pour les cibles loopback ou réseau privé. Le client HTTP partagé suivait également automatiquement les redirections.
Cela a transformé une fonctionnalité multimédia normale en une primitive SSRF d'origine serveur authentifiée, et est finalement devenu CVE-2026-45806.
Penpot : Penpot sur 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
Cela affectait Penpot. Sur son site officiel et son kit média, Penpot se présente comme ayant une base d'utilisateurs en croissance de plus d'un million et indique que des dizaines de milliers d'organisations l'utilisent, notamment Blender, Mozilla, Fedora, NTT Data, MIT, Société Générale, Cisco, Fujitsu, Indra et ByteDance.
éditeur de fichier authentifié -> URL d'image distante contrôlée par l'attaquant -> create-file-media-object-from-url -> récupération d'image backend download-image avec redirections activées -> requête finale atterrit sur un point de terminaison d'image interne uniquement -> SSRF d'origine serveur / atteignabilité interne
Penpot est une plateforme open-source de conception et de collaboration sur le code.
Elle gère des choses comme :
Cela signifie que son chemin d'importation multimédia se situe sur une véritable frontière de confiance.
La question importante n'était pas de savoir si Penpot prend en charge l'importation d'images distantes.
La vraie question était :
Penpot restreint-il l'endroit où le backend est autorisé à se connecter lorsqu'un utilisateur importe une image distante ?
Dans ce cas, ce n'était pas le cas.
Beaucoup de gens sous-estiment les fonctionnalités d'importation distante.
C'est une erreur.
Dès qu'une application :
elle crée une véritable frontière de confiance sortante.
C'était le problème ici.
Ce bug ne se situait pas dans le rendu d'image. Il ne se situait pas dans le stockage de fichiers. Il ne se situait pas dans les vérifications d'autorisations ordinaires pour l'édition d'un fichier.
C'était un classique échec de confiance côté serveur :
C'est suffisant pour créer une véritable vulnérabilité.
Je n'ai pas abordé Penpot en fuzzeant aveuglément des méthodes RPC aléatoires ou en recherchant d'abord des crashs.
L'approche plus solide consistait à identifier la frontière de sécurité la plus prometteuse.
Pour Penpot, c'était l'importation de médias distants.
Pourquoi ?
Parce que cette fonctionnalité combine :
C'était la bonne frontière à inspecter.
Et c'était exactement là où se trouvait le bug.
Le bug se réduit à une petite chaîne de confiance.
Dans le frontend :
(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}))
L'url contrôlée par l'utilisateur est envoyée directement dans l'appel RPC.
Ensuite, dans le backend :
(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))])
et :
(defn- create-file-media-object-from-url
[cfg {:keys [url name] :as params}]
(let [content (media/download-image cfg url)
le backend vérifie que l'appelant peut éditer le fichier cible, puis passe l'URL contrôlée par l'attaquant dans media/download-image.
L'implémentation de la récupération est ici :
(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})
Et le client HTTP partagé est configuré comme suit :
(http/build-client {:connect-timeout 30000
:follow-redirects :always}))
C'est toute la vulnérabilité :
Parce que l'attaquant a seulement besoin :
La chaîne d'attaque est simple :
C'est tout le bug.
La distinction importante est où la requête se produit.
La question n'est pas :
"Penpot peut-il importer des images à partir d'URL ?"
La vraie question est :
"Un utilisateur authentifié peut-il faire en sorte que le backend de Penpot se connecte à des destinations internes que l'utilisateur ne devrait pas pouvoir atteindre via l'application ?"
Dans ce cas, la réponse était oui.
Cela compte car il y a une réelle différence entre :
La validation d'image ne supprime pas cette différence.
Elle réduit certains cas d'exfiltration directe, mais elle ne supprime pas la condition SSRF ni la rupture de frontière réseau.
J'ai validé ce problème avec une preuve locale contrôlée liée directement au chemin de code Penpot examiné.
Le but n'était pas de toucher une infrastructure tiers. Le but était de prouver la propriété de sécurité exacte :
J'ai construit un validateur Java autonome qui reflétait le comportement pertinent :
content-type et content-lengthJ'ai validé deux cas.
Le validateur a demandé :
http://127.0.0.1:7790/internal.png
Résultat observé :
http://127.0.0.1:7790/internal.pnghttp://127.0.0.1:7790/internal.png200image/pngCela a prouvé que la logique de récupération de type importation acceptait un point de terminaison d'image interne uniquement directement.
Le validateur a ensuite demandé :
http://localhost:7791/redirect-to-internal
Ce point de terminaison a renvoyé une redirection HTTP vers :
http://127.0.0.1:7790/internal.png
Résultat observé :
http://localhost:7791/redirect-to-internalhttp://127.0.0.1:7790/internal.png200image/pngL'écouteur interne uniquement a enregistré la requête redirigée.
Cela a prouvé l'affirmation la plus importante :
La charge utile ici était intentionnellement simple :
Cela importait car Penpot ne se contente pas de récupérer des octets arbitraires et de s'arrêter. Il effectue une validation orientée média après la requête.
Donc, la bonne preuve n'était pas :
"le backend peut essayer de se connecter quelque part"
La preuve plus forte était :
"le backend peut être amené à se connecter quelque part d'interne et à terminer la requête avec succès dans les mêmes contraintes de type image attendues par la fonctionnalité"
C'est exactement ce que la validation a démontré.
Une réaction courante aux bugs SSRF comme celui-ci est :
"la cible doit toujours renvoyer une image"
Cette observation est vraie mais incomplète.
Elle ne supprime pas la vulnérabilité.
Elle vous indique simplement quelles cibles internes sont les plus directement utiles.
Ce problème permet toujours :
C'est toujours une véritable rupture de frontière de sécurité.
Surtout dans les environnements auto-hébergés, les services internes existent souvent spécifiquement derrière cette frontière.
Ce problème a finalement reçu une gravité Élevée CVSS :
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Cette classification a du sens.
L'affirmation n'est pas qu'un attaquant non authentifié peut immédiatement compromettre n'importe quel déploiement Penpot à partir de rien.
L'affirmation est que tout éditeur de fichier authentifié normal peut transformer Penpot en une primitive de requête backend vers des destinations internes, y compris l'accès assisté par redirection à des cibles loopback et réseau privé.
Il y a eu quelques discussions sur la gravité lors de la divulgation, principalement autour de :
Ce sont des contraintes justes à discuter.
Mais elles ne suppriment pas le problème central :
C'est une vulnérabilité SSRF réelle et défendable.
Le correctif important ici n'est pas une gestion plus stricte de MIME.
Le vrai correctif est la politique de destination sortante.
Une remédiation correcte pour cette classe de bug doit :
http et httpslocalhostC'est la bonne direction de correctif car ce n'était pas un bug d'analyse d'image. C'était un bug de frontière de confiance réseau.
Ce problème a été signalé de manière privée via le flux de signalement de sécurité de GitHub.
Le rapport comprenait :
Les mainteneurs ont confirmé le problème et ont commencé à travailler sur une résolution.
Le problème a ensuite été attribué :
CVE-2026-45806
La leçon clé ici est simple :
l'importation de médias distants est une frontière de confiance sortante, pas seulement une fonctionnalité de commodité
Beaucoup de développeurs pensent en termes de :
Ce sont des détails d'implémentation.
La vraie question de sécurité est :
où le backend est-il autorisé à se connecter au nom d'un utilisateur ?
Si cette question n'est pas répondue explicitement, des fonctionnalités comme l'importation distant deviennent des surfaces SSRF par défaut.
Ce bug renforce également quelque chose d'important dans l'examen SSRF :
C'est le véritable enseignement.
Cette vulnérabilité ne concernait pas une charge utile flashy.
Il s'agissait de poser la bonne question sur la frontière de confiance.
Penpot permettait à un éditeur de fichier authentifié de fournir une URL d'image distante, et le backend faisait trop confiance à cette URL. La gestion des redirections a fait le reste.
C'est pourquoi cela est devenu CVE-2026-45806.