Est-il possible de contourner la fonction PHP htmlspecialchars() ?
29 Juillet 2022
La fonction htmlspecialchars() est fréquemment utilisée pour nettoyer les entrées utilisateur dans les applications PHP natives (comprendre sans utilisation de framework). Est-elle suffisante pour couvrir tous les cas d'utilisation ? C'est ce que nous allons tenter de découvrir dans cet article.
La fonction htmlspecialchars()
Avant d'analyser la méthode htmlspecialchars(), voici le code d'une injection simple au sein d'une page HTML :
Le navigateur exécute le code Javascript renseigné dans le paramètre param présent dans l'URL (il s'agit donc d'une XSS réfléchie) :
Si le développeur souhaite protéger son application (et ses visiteurs) de cette vulnérabilité, il se doit d'assainir la donnée non fiable (la valeur du paramètre param) avant son affichage. C'est ce que propose de faire la fonction htmlspecialchars().
htmlspecialchars() est une fonction PHP permettant de se protéger contre les injections de type Cross-Site Scripting (XSS) en convertissant certains caractères spéciaux en entités HTML :
Le premier paramètre est la chaîne à convertir. Le second un masque de plusieurs drapeaux (dont la valeur par défaut est ENT_COMPAT, ou ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 à partir de la version >= 8.1.0 de PHP) qui peut prendre une ou plusieurs des valeurs suivantes :
Sauf quand indiqué, la version de PHP utilisée est ici la 7.4.30.
Le troisième, facultatif, est le jeu de caractères qui sera utilisé lors de la conversion. Par défaut, les versions récentes utilisent l'encodage "UTF-8" (modifiable dans le fichier de configuration php.ini). Les jeux de caractères supportés sont les suivants :
Le dernier paramètre est un booléen dont la valeur par défaut est true, qui spécifie si une entité HTML présente en entrée doit à nouveau être convertie (par exemple <, soit le caractère <, donnera &lt;) ou laissée telle quelle.
En général, il est relativement facile d'identifier une telle transformation. Voici l'exemple d'injection simple vu précédemment, mais cette fois, avec un assainissement effectué grâce à htmlspecialchars() :
GET /minipoc/htmlspecialchars/?param=%3Cscript%3Ealert(1)%3C/script%3E HTTP/1.1Host:192.168.56.101User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Connection:close
Mais dans la réponse HTTP, les entités HTML sont visibles (également visible dans le code source via un clic-droit -> "Afficher le code source de la page") :
L'injection ne semble plus possible, et la valeur du paramètre param est affichée correctement :
L'article ne traite pas le cas de la fonctionhtmlentities() de PHP mais peut également s'y appliquer.
Limites et contournements
Les attributs HTML "src" et "href"
Lorsque la donnée non fiable est utilisée en tant que valeur des attributs HTML "src" ou "href", son assainissement par la fonction htmlspecialchars() ne fonctionnera pas :
Etant donné que l'interpréteur est ici le moteur Javascript et non le moteur de rendu HTML, les entités HTML sont tout de même interprétées.
GET /minipoc/htmlspecialchars/x HTTP/1.1Host:192.168.56.101User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Accept:image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8Referer: http://192.168.56.101/minipoc/htmlspecialchars/?param=javascript:document.write(%22%3Cimg%20src=x%20onerror=alert(1)%3E%22)
Connection:close
Soit lors du clic de la victime :
Cet exemple est aussi valable pour l'attribut HTML "src" avec, par exemple, la balise <iframe></iframe>.
Les évènements HTML
Les évènements HTML ne peuvent être assainis correctement par la méthode htmlspecialchars() car l'exécution s'effectue dans un contexte Javascript. Ici un exemple avec l'évènement onlick :
<!DOCTYPE html><html lang="fr"><head><title>Evènement HTML</title></head><body> <p><?php echo '<div onclick=' . htmlspecialchars($_GET['param']) . '>Lorem ipsum dolor sit amet, ...</div>'; ?></p>
</body></html>
GET /minipoc/htmlspecialchars/?param=alert(1) HTTP/1.1Host:192.168.56.101User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Connection:close
HTTP/1.1 200 OKServer: Apache/2.4.38 (Debian)Connection: closeContent-Type: text/html; charset=UTF-8<!DOCTYPEhtml><htmllang="fr"> <head> <title>Evènement HTML</title> </head> <body> <p><div onclick=alert(1)>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div></p>
</body></html>
Soit lors du clic de l'utilisateur sur le paragraphe :
Les attributs "safe" HTML
Les attributs "safe" sont des attributs HTML n'exécutant pas de code JavaScript, contrairement au évènements HTML. Voici la liste de ces attributs :
Cela provient du fait que le développeur n'a pas renseigné tous les paramètres nécessaires à la fonction. La fonction, par défaut, ne transforme pas le caractère ' si le drapeau ENT_QUOTES n'est pas renseigné :
Grâce à cette modification, l'application semble être maintenant protégée :
Cas n°3
Cette fois, le développeur attribue bien la bonne valeur de drapeau a la fonction d'assainissement, mais n'entoure pas les valeurs des attributs HTML avec les caractères " ou ' :
La version 8.1.0 introduit une modification dans la valeur par défaut des drapeaux de la fonction htmlspecialchars() :
Ce changement a pour effet de ne plus rendre vulnérable le cas n°2 dans le cas de l'omission du second paramètre.
Le jeu de caractères (encoding)
Je n'ai ici trouvé que très peu de ressources sur d'éventuelles vulnérabilités quant au jeu de caractères présent en troisième paramètre de la fonction, voici un début de trouvaille sur IE11.
Le troisième paramètre est le jeu de caractères utilisé lors de la conversion. Par défaut, il prend la valeur définit dans le fichier php.ini, soit généralement "UTF-8" pour les versions de PHP assez récentes.
Le code suivant permet une injection XSS malgré l'utilisation de la fonction htmlspecialchars(), mais cela ne semble fonctionner que sous IE 11 :
<<!DOCTYPE html><html lang="fr"><head><title>XSS in IE11</title></head><body><p><?php header('Content-Type: text/html;charset=UTF-7');echo"xss encoding : ".htmlspecialchars($_SERVER["QUERY_STRING"],ENT_QUOTES,"UTF-8");?></p></body></html>
Deux limitations à cette exploitation :
Elle ne semble pas fonctionner lorsque la valeur du paramètre est récupérée depuis $_GET['param']
L'attaquant doit trouver un moyen de forcer l'encodage de la page à "UTF-7" (codé en dur dans l'exemple : header('Content-Type: text/html;charset=UTF-7'); )
Pour exploiter la vulnérabilité, l'attaque transforme tout d'abord son injection en "UTF-7". Ici <script>alert(1)</script> donne +ADw-script+AD4-alert(1)+ADw-/script+AD4- :
Cette exploitation possède par contre l'avantage de contourner le filtre anti-XSS d'IE 11 :
GET /minipoc/htmlspecialchars/indexie.php?xss=+ADw-script+AD4-alert(1)+ADw-/script+AD4- HTTP/1.1Accept:text/html, application/xhtml+xml, image/jxr, */*User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like GeckoHost:192.168.56.101Connection:close
L'utilisation de la fonction htmlspecialchars() n'est donc pas si simple que cela, et nécessite de bien réfléchir à son contexte d'utilisation. PHP 8.1.0 améliore les choses en modifiant les drapeaux par défaut et en évitant ainsi une possibilité d'injection. Le mieux, reste sans doute de bien renseigner les les trois paramètres possibles, par exemple :
Il y a également d'autres emplacements non couverts dans cet article mais qui nécessite une attention particulière (comprendre par là que htmlspecialchars() ne sera sans doute pas suffisant comme protection). Par exemple au sein de code Javascript ou encore de code CSS.