title: Cloudbleed
date: 2017-02-24
tags: Actualité, Internet
url: cloudbleed
slug: cloudbleed


![Logo représentant un nuage aux couleurs de cloudflare, mais les gouttes de pluie sont remplacées par des gouttes de sang \(référence à heartbleed, le cœur qui saigne\). En dessous se trouve le texte #cloudbleed.](/images//cloudbleed.png)

### Cloudbleed ?

Cloudbleed est le nom donné à cette faille, nom plus facile à retenir que
project-zero:1139.
<https://bugs.chromium.org/p/project-zero/issues/detail?id=1139>
<https://blog.cloudflare.com/incident-report-on-memory-leak-caused-by-
cloudflare-parser-bug/>

### Qu'est-ce que Cloudflare ?

Cloudflare est un service de reverse proxy (serveur cache + protection contre
les attaques de type DDOS), un service de CDN (réplication de contenu partout
dans le monde sur des serveurs plus proches physiquement des utilisateurs), un
service d'optimisation des pages (réduction/réécriture).

L'infrastructure de Cloudflare est essentiellement basée sur Nginx (serveur
web + reverse proxy, comme Apache ou lighttpd, mais en plus léger et rapide).

### Est-ce lié à Heartbleed ?

Non. Même si l'importance de cette faille a plus ou moins le même impact que
Heartbleed (fuite de données).

### La faille, en détails

#### Provocation de la faille

Une page html, hébergée derrière un service proposé par cloudflare, pesant
moins de 4 ko (4096 octets) et contenant des données mal formatées (balise
fermante absente), peut contenir des données provenant de la mémoire (RAM) du
serveur.

Ces données peuvent être des clefs de chiffrement, des cookies
(d'authentification, par exemple), des mots de passe, des fragments de données
de type POST (données envoyés depuis un formulaire web, que ça soit un panier
sur un site marchand, un couple identifiant/mot de passe, un commentaire sur
un blog…), voire même des requêtes https provenant d'autres sites utilisant
également cloudflare.

Ces données ont été indexées par les moteurs de recherche (google, bing,
yahoo, yandex…), et sont encore accessible dans leurs cache (le vidage des
caches contenant ces données sont en cours).

Le problème provient d'un bout de code source, utilisé par email obfuscation
(masquage d'adresse email), Server-side Excludes (Exclusions côté serveur) et
automatic HTTPS rewrites (remplacement à la volée des adresses http:// en
https:///.

En résumé, une page web est lue par le serveur nginx de cloudflare, ce serveur
réécrit une partie de la page, par exemple, pour masquer une adresse email,
puis envoie cette page au navigateur web.

Le problème est situé au moment où le contenu est réécrit.

#### Comment le contenu est-il réécrit ?

Depuis les débuts de cloudflare, les ingénieurs utilisaient Ragel, un langage
de programmation basé sur des expressions régulières, qui génère du code en
langage C (transcompilation, conversion d'un langage de programmation vers un
autre langage de programmation. En général, on utilise cette technique de
programmation pour pouvoir écrire le code en un langage relativement simple,
ou contenant certaines fonctionnalités (langage dit de haut-niveau). Ce code
est alors converti en un autre langage, plus "proche de la machine" (langage
dit de bas niveau)).

Ragel : <https://www.colm.net/open-source/ragel/>

Les expressions régulières sont donc converties en C, jusque là, pas de
problème. Sauf qu'avec le temps, le code écrit avec Ragel a augmenté en
longueur et en complexité (de plus en plus de règles à évaluer), et devenait
de plus en plus difficile à maintenir. Les ingénieurs décidèrent alors
d'écrire un nouveau code (appelé cf-html) pour le remplacer. Il s'est avéré
que cf-html était beaucoup plus rapide, fonctionnait correctement avec le HTML
5 et était facile à maintenir (du code facile à maintenir est du code facile
et rapide à corriger et à améliorer).

La première fonctionnalité à utiliser cf-html est la fonctionnalité qui
corrige les adresses http en https. Les autres fonctionnalités, sont migrées
progressivement.

L'un des points à noter, c'est que le code généré par Ragel et cf-html est
compilé en tant que modules, puis est chargé par ngnix.

Chaque module chargé par le serveur proxy analyse le contenu html dans des
blocs de mémoire, effectue des modifications si nécessaire, et passe le
contenu au module suivant. Lorsque tous les modules ont effectués leurs
traitements, le serveur ngnix envoie la page au navigateur web.

Parmi ces traitements, cela peut être le déchiffrement du contenu https,
l'analyse de données (vers qui est destiné cette requête), le contenu est-il
encore à jour (cache expiré ou non)…

Une fois le bug isolé, il s'est avéré qu'il provenait de cf-html et non de
Ragel. Cependant, le bug était également présent dans le code écrit en Ragel,
mais il n'y avait aucun impact, parce que la mémoire était correctement
traitée. Via cf-html, il s'est avéré que la mémoire était traitée de manière
légèrement différente (de manière subtile) par rapport à Ragel, et c'est de là
que provient la faille.

#### Désactivation des services concernés

Dès que cloudflare a déterminé que le problème venait de cf-html, la société a
immédiatement désactivé les services dépendants de ce module, en attendant de
comprendre exactement d'où venait le problème.

#### Origine du problème

Si vous lisez couramment l'anglais (technique), je conseille vivement la
lecture de ce paragraphe (et celui d'après).
<https://blog.cloudflare.com/incident-report-on-memory-leak-caused-by-
cloudflare-parser-bug/#rootcauseofthebug>

Il est nettement plus détaillé que mes explications plus bas. Mes explications
sont plus génériques (tentative de vulgarisation des éléments techniques avec
exemple d'un débordement).

Le code généré est du code en langage C. Ce langage, de bas niveau, permet la
manipulation de « pointeurs », et d'accéder directement à la mémoire de la
machine (bon, avec quelques restrictions, je simplifie parce que c'est plutôt
compliqué en fait. Et il faudrait tout un livre pour expliquer les pointeurs).

le code posant problème est le suivant :

> /* generated code */
>  if ( ++p == pe )
>  goto _test_eof;
>

p correspond au pointeur (où on se trouve actuellement).
pe : l'adresse mémoire de la fin de la zone à lire.

++p : on ajoute 1 à p (on se déplace d'un octet)
puis on compare p avec pe.
Si p est égal à pe alors on saute à l'étiquette _test_eof.
Sinon on continue la lecture.

À noter : EOF signifie End Of File (Fin de fichier).

#### Ok, et en quoi ce code pose problème ?

Déroulons l'exécution :

Le texte html est stocké entre les adresses 0 et 4096 (4 ko). On ne tient pas
compte si c'est de l'utf-8 (pour l'exemple).
On initialise le pointeur p à zéro, et pe à 4096.

> on définit une étiquette _begin
>
> On lit l'octet à l'adresse stockée dans p (p étant un pointeur vers cette
> adresse).
>  On fait le traitement requis.
>  On incrémente p
>  p contient désormais 1
>  On compare p avec pe
>  p -> 1, pe -> 4096.
>  Les résultats sont différents, on continue le traitement.
>
> On va à l'étiquette début.
>
> On lit l'octet à l'adresse stockée dans p.
>  On fait le traitement requis.
>  On incrémente p
>  p contient désormais 2
>  On compare p avec pe
>  p -> 2, pe -> 4096.
>  Les résultats sont différents, on continue le traitement.
>
> On va à l'étiquette début.

ainsi de suite…

on arrive au moment ou p contient 4095

> On lit l'octet à l'adresse stockée dans p.
>  On fait le traitement requis.
>  On incrémente p
>  p contient désormais 4096
>  On compare p avec pe
>  p -> 4096, pe -> 4096.
>  Les résultats sont identiques, on va à l'étiquette _test_eof.

Ici, le code fonctionne sans problème.

Que se passerait-il si, par hasard, on ajoute 1 entretemps ?

Admettons que l'on soit dans la boucle, et p = 4094

> On lit l'octet à l'adresse stockée dans p.
>  On fait le traitement requis.
>  On incrémente p
>  p contient désormais 2
>  On compare p avec pe
>  p -> 4095, pe -> 4096.
>  Les résultats sont différents, on continue le traitement.

pour une certaine raison, on incrémente p de 1.

p vaut alors 4096.

Vous voyez où est le problème ? Non ? On continue le déroulement.

p vaut 4096.

> On lit l'octet à l'adresse stockée dans p.
>  On fait le traitement requis.
>  On incrémente p
>  p contient désormais 4097
>  On compare p avec pe
>  p -> 4097, pe -> 4096.
>  Les résultats sont différents, on continue le traitement.

On commence à lire les données situées dans les adresses mémoire au delà de
4096, comme défini plus haut.

Et là, c'est le drame. On vient de sortir de la zone mémoire dans laquelle on
était censé lire les données (nom de ce bug : buffer overrun).

La solution pour éviter ce genre de problème est toute simple.

Le code effectuant le test est

> if ( ++p == pe )
>     goto _test_eof;

Il suffit de remplacer le test de l'égalité == par est supérieur ou égal à

> if ( ++p >= pe )
>     goto _test_eof;

Ainsi, lors de la lecture de la mémoire, si jamais le pointeur p est supérieur
ou égal à l'adresse pe, on sortira systématiquement en allant à l'étiquette
_test_eof.

Mais, le problème de cloudfront, c'est que ce code n'est pas écrit par un·e
humain·e, ce code est du code généré via Ragel.

Le code écrit en Ragel est le suivant :

> script_consume_attr := ((unquoted_attr_char)* :>> (space|'/'|'>'))
>  >{ ddctx("script consume_attr"); }
>  @{ fhold; fgoto script_tag_parse; }
>  $lerr{ dd("script consume_attr failed");
>  fgoto script_consume_attr; };
>

On essaie de lire un attribut dans une balise, par exemple %lt;script
type="javascript">
Si l'attribut contient un espace, un slash ou un &gt; alors on est à la balise
de fin.
Si l'attribut est correctement formaté, alors on exécute le code situé dans
@{}. Sinon, on exécute le code situé dans $lerr{}.

Admettons que dans la page, le contenu soit tronqué, et contienne uniquement
&lt;script type=
Dans ce cas, le code situé dans $lerr{} sera appelé. À l'intérieur, on trouve
simplement "fgoto script_consume_attr", qui fait en sorte que le code saute à
l'étiquette script_consume_attr, qui est définie juste au dessus. On entre
dans une boucle (infernale), on sort de la zone mémoire attendue et on
commence à lire le contenu de la mémoire qui ne nous est pas destiné (buffer
overrun).

La solution ici, est de rajouter une instruction fhold dans le bloc $lerr{}.
fhold est l'équivalent en C de p-- (on décrémente p de 1). Du coup, p contient
le caractère qui a provoqué l'erreur :)

Je pense que c'est assez compliqué comme ça, du coup, je vais arrêter là pour
l'origine du problème.

### Qui est impacté ?

La liste, encore incomplète, des sites potentiellement impactés, est
disponible ici :
<https://github.com/pirate/sites-using-cloudflare>

Il est possible de télécharger le fichier complet (> 22 Mio) et de chercher à
l'intérieur.

### L'ironie de l'histoire

Cloudflare a bien un programme de chasse aux bugs, pour tout bug découvert, on
peut gagner un t-shirt.

En comparaison, quand on voit les montants proposés par Google, ça laisse
songeur…
<https://sites.google.com/a/chromium.org/dev/Home/chromium-security/hall-of-fame>