Délinéarisation et injection d’objet en PHP
Divulgâchage : Un jour, on a inventé l’idée un peu folle de transférer des objets d’une application à l’autre. Par la suite, on s’est rendu compte que cette possibilité ouvrait de grandes possibilités d’attaque pour qui peut injecter son propre contenu. Finalement, on s’est rendu compte qu’on ne devrait jamais utiliser la délinéarisation.
Il fut un temps, révolu, ou développer un site web consistait à
proposer une interface graphique sympa pour faciliter la vie des
utilisateurs. Il suffisait d’agrémenter du HTML
avec des
instructions PHP
pour donner à son site un je ne sais
quoi qui dynamise l’ensemble et rende le monde meilleur.
Puis on s’est rendu compte qu’on pourrait carrément faire communiquer des applications entre elles. S’ouvrait alors tout un univers de possibles… Enrichir un service tiers, déléguer des fonctionnalités (comme l’authentification), unifier plusieurs plateformes,… Et pour permettre à tout ce petit monde de se comprendre, on a inventé plein de formats de données.
Dans notre frénésie créative, on a inventé la (dé)linéarisation, un moyen de transférer nativement des structures complexes, dont des objets, d’une application à l’autre. C’était sûrement une idée géniale sur le coup (parce que super pratique pour les développeurs), mais comme on va le voir aujourd’hui, c’est sûrement la dernière chose que vous devriez utiliser dans une application.
Bon, en vrai, je préférerais encore délinéariser que de gérer du XML. Heureusement, d’autres technologies on été inventées et nous évitent de devoir recourir à de telle extrémités.
Le problème de la (dé)linéarisation, c’est qu’elle permet à vos utilisateurs (humains ou pas), de forger leurs propres objets dans votre application. Biens choisis, certains objets auront des comportements pas vraiment prévu et permettront à ces petits malins de détourner votre application à leur avantage.
Cette fois, la solution est simple, si vous voyez un appel à
unserialize()
, ou un truc qui y ressemble, même de loin :
fuyez ; le terrain est miné. Si vous pensez pouvoir vous en sortir en
faisant attention, demandez une assistance psychologique, votre vie est
peut être en danger.
Les objets en PHP
Cette vulnérabilité étant typique du monde des objets,
quelques rappels préliminaires peuvent être utiles si vous n’avez pas
l’habitude de développer de cette manière. J’utilise ici du
PHP
, mais le fonctionnement serait sensiblement le même
dans un autre langage objets.
Si vous ne savez pas ce qu’est un objet, dites-vous que c’est un truc qui contient à la fois des données (des variables qu’on appelle attributs ou membre) et des fonctionnalités (des fonctions qu’on appelle méthodes). Votre programme est plein de ces trucs qui interagissent les uns avec les autres pour, globalement, résoudre votre problème et vous fournir les résultats que vous attendez.
Personnellement, je vois vraiment les programmes objets comme des écosystèmes plein de petites bêtes (mes objets) qui vivent leur vie et interagissent les uns avec les autres. Lorsque je programme, je définit les espèces des bestioles avec leurs caractéristiques et leurs comportements.
Ça a un côté démiurge, mais depuis que je consulte, ça va mieux.
La plupart du temps, ces objets sont décrit dans ce qu’on appelle des classes, sortes de patrons, modèles, moules ou toute autre métaphore qui signifie qu’un objet est créé en mémoire et sera manipulé d’après ce qui est écrit dans la classe. La programmation orientée objet consiste donc d’une part à définir des classes puis à créer des objets (on dit instancier) et enfin à lancer les interactions.
« La plupart du temps » parce que certains langages, comme javascript, boudent ce formalisme et préfèrent ajouter les attributs et les méthodes à la volée sans aucune notion de type, les objets étant créés par clonage, c’est un joyeux bordel créatif. Ça ne les rend pas invulnérables (que du contraire), l’approche est juste différente.
Pour la bonne cause, voici une petite classe, décrivant des utilisateurs très simples dont le seul but est de se répondre poliment lorsqu’ils se saluent… Je documente ici avec doxygen plus pour l’exemple que la nécessité.
/**
* Classe décrivant les utilisateurs
*/
class User {
/**
* Attribut stockant le nom de l'objet
*/
public $name ;
/**
* Constructeur, pour initialiser un nouvel objet
*
* @param $name le nom de l'objet
*/
public function __construct(string $name) {
$this->name = $name ;
}
/**
* Méthode pour connaître le nom de l'objet
*
* @return le nom de l'objet
*/
public function whoAreYou() {
return $this->name ;
}
/**
* Méthode permettant de saluer l'objet
*
* @param $other l'objet qui salue
* @return la réponse de l'objet
*/
public function hello(User $other) {
return "Nice to meet you " . $other->name
. ", I am " . $this->name ;
} }
Une fois cette classe définie et son code chargé par vos scripts, vous pouvez l’utiliser pour créer des objets et les faire interagir.
// Création de deux objets
$foo = new User("Foo") ;
$bar = new User("Bar") ;
// Appel de méthode
echo $foo->hello($bar) ;
// > Nice to meet you Bar, I am Foo
La linéarisation
Aussi appelée sérialisation (américanisme de
serialization), ça consiste à transformer des objets (parfois
complexes) en une chaîne de caractère (ou une suite de
et de
).
Notez que le but, ici, est d’effectuer plus tard la transformation
inverse, on parle alors de délinéarisation, ou désérialisation. La
documentation PHP
résume bien la chose en parlant de générer une
représentation stockable.
Le truc, c’est que copier simplement la mémoire ne marchera pas. D’abord parce que les objets nécessaires peuvent être réparti un peu partout. Ensuite, parce que l’ensemble de la mémoire nécessaire peut contenir des données inutiles (voir sensibles, ce qui serait dommage). Enfin, les objets peuvent contenir des ressources non stockables, comme des descripteurs de fichiers, des connexions à des bases de données,…
On a donc du mettre au point des techniques spécifiques pour sauvegarder ces données complexes.
Nativement
En PHP
, la linéarisation est effectuée nativement avec
les deux fonctions suivantes :
- Linéarisation : avec la fonction serialize() qui prend en paramètre la donnée à sérialiser et vous retourne une chaîne de caractère,
- Délinéarisation avec la fonction unserialize() qui effectue l’opération inverse en prenant une chaîne de caractère et vous fournissant les données correspondantes.
Pour compléter, PHP
fourni d’autres moyens pour
transformer vos objets en chaînes (et vice versa). var_export()
génère un code source PHP
qu’il faut interpréter
pour retrouver vos données (très dangereux donc). Ou encore json_encode()
et sa réciproque json_decode()
mais dans ce cas on perd complètement le typage.
Pour être vraiment complet, des fous ont même inventé une méthode à base de XML… Après avoir installé l’extension PEAR XML Serializer, vous pourrez exporter et importer vos objets depuis le format XML (au pris de 3 points de santé mentale).
Pour revenir à la linéarisation native, voyons ce que ça donnerait avec nos utilisateurs :
$foo = new User("Foo") ;
echo serialize($foo) ;
// O:4:"User":1:{s:4:"name";s:3:"Foo";}
Si vous voulez comprendre le sens profond de cette linéarisation, le voici :
O:4:User:
signifie qu’on est en présence d’un objet linéarisé (O
), le nom de la classe fait 4 caractères et vautUser
,1:{...}
nous donne le nombre d’attributs de l’objet (ici, 1), ceux-ci sont linéarisés les uns après les autre entre les accolades,s:4:"name";
signifie que le nom du premier attribut fait 4 caractère et vautname
,s:3:"Foo";
signifie que la valeur du premier attribut est une chaîne, de 3 caractères, valantFoo
.
Et il est tout aussi facile de délinéariser les données précédentes :
$string = 'O:4:"User":1:{s:4:"name";s:3:"Foo";}' ;
$foobis = unserialize($string) ;
var_dump($foo == $foobis) ; // bool(true)
var_dump($foo === $foobis) ; // bool(false)
Après délinéarisation, on obtient un deuxième objet, équivalent au
premier mais distinct. L’opérateur d’égalité ==
portant sur
la classe et le contenu des attributs nous dit donc qu’ils sont
égaux. L’opérateur identique ===
portant sur la
référence des objets nous dit, par contre, qu’ils sont distincts.
Spécialisation
Lorsque vos objets sont trop complexes pour être linéarisés
automatiquement, PHP
vous fourni deux moyens de surcharger
ses mécanismes afin d’utiliser les vôtres.
- Via les méthodes magiques :
__sleep()
et__wakeup()
(cf. doc officielle). La première est appelée avant la linéarisation et est sensée retourner la liste des noms des attributs à linéariser. La deuxième est appelée juste après avoir délinéarisé l’objet (sans paramètre puisque les attributs ont été restaurés). - En implémentant une interface : Serializable qui vous demande d’implémenter deux méthodes. serialize() qui retourne la représentation de votre objet, et unserialize() qui fait l’inverse.
Par exemple, avec nos utilisateurs, si on voulait enregistrer le nombre de générations qui séparent une copie de l’objet original, voici le genre de code qu’on pourrait produire.
class User implements Serializable {
// Previous code here
public $generation = 0 ;
public function serialize() {
$data = [$this->me, $this->generation + 1] ;
return serialize($data) ;
}
public function unserialize($string) {
$data = unserialize($string) ;
list($this->me, $this->generation) = $data ;
} }
Bon à savoir, si vous implémentez l’interface
Serializable
, les méthodes magiques __sleep()
et __wakeup()
seront ignorées.
Pour ceux qui font du RAII, sachez que la linéarisation vous demandera quelques précautions…
- Une fois linéarisé, l’objet existe encore, son destructeur sera donc appelé normalement,
- Lors de la délinéarisation, le constructeur n’est pas appelé (puisqu’on considère qu’on remet un objet « en l’état »).
Pourquoi faire ? La sauvegarde
Le premier intérêt de linéariser des objets est qu’il permet leur stockage puis leur restauration. Ainsi, on peut à tout moment sauvegarder l’état des objets dans des fichiers puis les restaurer en cas de besoin depuis ces fichiers de sauvegarde.
On pourrait imaginer que dans notre exemple, on commence d’abord par sauvegarder nos objets dans des fichiers.
file_put_contents("foo.txt", serialize(new User("Foo"))) ;
file_put_contents("bar.txt", serialize(new User("Bar"))) ;
Puis, plus tard dans le script (ou dans un autre scripts d’ailleurs), de les récupérer et poursuivre les calculs.
$foo = unserialize(file_get_contents("foo.txt")) ;
$bar = unserialize(file_get_contents("bar.txt")) ;
echo $foo->hello($bar) ;
// Nice to meet you Bar, I am Foo
Cet exemple utilise des fichiers mais la linéarisation peut aussi être utilisée pour sauvegarder des informations dans une base de donnée. Par contre, comme on va le voir plus loin, les cookies, sont une très mauvaise idée.
Pourquoi faire ? Le transfert
En plus de restaurer une sauvegarde locale, la linéarisation permet aussi de transférer des objets d’une application à l’autre. Les calculs pouvant alors être répartis en fonction des ressources disponibles ou de la logique de l’algorithme.
On pourrait imaginer que dans notre exemple, l’appel à
hello()
se fasse sur un serveur spécifique. Le script
correspondant restaurerait les objets qui lui sont passés en paramètre
(getvars ici mais tout est envisageable) avant d’appeler la
méthode idoine.
$foo = unserialize($_GET["foo"]) ;
$bar = unserialize($_GET["bar"]) ;
echo $foo->hello($bar) ;
Dans ce cas, la création des objets se ferait dans un autre scripts, peut être sur un autre serveur à l’autre bout du monde. La linéarisation permettant de passer l’état des objets d’un serveur à l’autre.
echo file_get_contents(
"http://example.com/hello.php"
. "?foo=" . urlencode(serialize(new User("Foo")))
. "&bar=" . urlencode(serialize(new User("Bar")))
;
) // Nice to meet you Bar, I am Foo
Je vous l’accorde, c’est un peu exagéré dans notre cas mais si vous disposez de ressources très particulières sur un serveur et ne voulez pas le surcharger avec des calculs accessoires (i.e. un HSM pour hacher des informations), l’idée n’est plus si incongrue que ça.
Comme on va le voir dans la suite, utiliser la linéarisation est une mauvaise idée si vous ne pouvez établir de confiance entre les deux serveurs. Dans une optique de défense en profondeur, la linéarisation pour transférer des objets est aussi une mauvaise idée.
Exploitation
Maintenant qu’on a vu que la (dé)linéarisation, c’est super, on va voir à quel point c’est une mauvaise idée : chaque fois que vous délinéariserez une chaîne venant d’un attaquant, vous lui permettrez d’injecter ses propres objets. Comme on va le voir, ça peut lui ouvrir des accès ou exécuter du code…
Exemple
Commençons par une application très simplifiée qui réutilise notre
classe User
. Admettons que lors de l’authentification,
l’Identity Provider stocke l’utilisateur dans un cookie.
Quelque chose dans ce genre :
$_COOKIE["user"] = serialize(new User($username)) ;
On pourrait alors imaginer qu’une autre partie de l’application, le Service Provider fasse un contrôle d’accès et ouvre des fonctionnalités restreintes s’il s’agit de l’administrateur :
$user = unserialize($_COOKIE["user"]) ;
if ($user->name == "admin") {
// God mode !
// ...
}
Pour un utilisateur qui ne triche pas, le cookie est généré par
l’application et le nom ne vaut admin
que s’il s’agit d’un
administrateur.
Modifier le comportement
Un attaquant, par contre, peut tricher et envoyer ce qu’il veut. Soit en changeant des caractères dans la linéarisation, soit en créant son propre objet qu’il linéarise ensuite. Dans les deux cas, il pourrait créer cette chaîne :
'O:4:"User":1:{s:4:"name";s:5:"admin";}'
Avec cette chaîne, l’objet délinéarisé aura le bon nom, nous ouvrant les portes des fonctionnalités restreintes !
Si vous pensez que le problème est dans la classe User
,
et qu’on devrait faire être plus prudents lors de la (dé)linéarisation,
je vais vous montrer que non.
Si vous pensez que le problème vient que le cookie soit en clair, il y a du vrai, mais on verra à la fin pourquoi même ça, je le déconseille.
Admettons qu’on interdise carrément la valeur admin
avec
quelque chose de ce genre :
class User {
// Previous code here
public function __wakeup() {
if ($this->name == "admin") {
throw new Exception("Admin can not be unserialize") ;
}
} }
Un attaquant peut très bien vous envoyer autre chose qu’un
User
. N’importe quel autre type disposant d’un
attribut name
valant admin
fera l’affaire. Au
pire, on peut même se rabattre sur le type stdClass
(la classe
native de PHP
pour tous les objets sans classe
spécifique) et lui ajouter manuellement l’attribut :
$o = new stdClass();
$o->name = "admin" ;
echo serialize($o) ;
// O:8:"stdClass":1:{s:4:"name";s:5:"admin";}
Avec une valeur comme celle-ci, le script ne délinéarise plus un
User
mais un stdClass
. Comme le
PHP
n’est pas très typé, ça ne lui posera pas de soucis, la
condition pourra être évaluée et nous ouvrir les portes…
Le problème ne vient pas de la classe User
mais du fait
d’avoir délinéarisé un objet venant d’un utilisateur. S’il est hostile,
il peut va injecter les objets qu’il veut pour son propre
bénéfice.
Exécuter du code
À ce stade, vous pourriez vous dire qu’on devrait être encore plus prudent et, par exemple, utiliser des méthodes plutôt que des attributs puisqu’un attaquant ne peut pas en ajouter.
Dans ce cas, admettons qu’aucune classe, nulle part dans votre
application, n’ait de méthode whoAreYou()
. Vous pourriez
vous dire que le code suivant est sûr :
$user = unserialize($_COOKIE["user"]) ;
if ($user->whoAreYou() == "admin") {
// God mode !
// ...
}
L’appel à la méthode va échouer si l’attaquant utilise autre chose
qu’un User
. La situation est donc sous contrôle !?
non
L’idée, cette fois, n’est plus de contourner la condition pour obtenir un accès privilégié, mais d’injecter des objets qui ont des méthodes utiles et s’arranger pour qu’elles soient appelées. Et le truc, c’est que même si votre code n’appelle pas beaucoup de méthodes, certaines méthodes magiques sont en fait systématiquement appelées :
__wakeup()
ouunserialize()
pour les classe qui spécialisent ce comportement,__destruct()
lorsque les données délinéarisées ne serviront plus.
Restons donc sur les méthodes magiques classiques et admettons qu’on dispose, quelque part dans l’application, d’une classe qui s’occupe de journaliser des événements dans des fichiers. Quelque chose de ce genre :
class Logger {
private $filename ;
private $buffer ;
public function __construct($filename) {
$this->filename = $filename ;
$this->buffer = "" ;
}
public function log($message) {
$this->buffer .= "$message\n" ;
}
public function __destruct() {
file_put_contents(
$this->filename,
$this->buffer, FILE_APPEND
;
)
} }
Cette classe définit des objets qui ont pour but de temporiser les messages d’événements pour les ajouter d’un seul coup dans un fichier journal spécifique. Rien de compliqué, c’est très classique comme genre de chose.
Si vous voulez jouer avec cette vulnérabilité, je vous conseille le challenge 26 de Natas qui vous laisse mettre en œuvre une injection d’objet via une classe très proche de celle-ci (vous devrez adapter).
Pour l’attaquant, cette classe est un cadeau. Il lui suffit
d’injecter un objet Logger
dans votre application. Comme il
maîtrise les attributs filename
et buffer
, il
pourra écrire ce qu’il veut et où il veut lorsque le destructeur sera
appelé…
// Code chez l'attaquant
class Logger {
public $filename ;
public $buffer ;
}
$payload = new Logger() ;
$payload->filename = '/var/www/index.php' ;
$payload->buffer = '<?php echo "Hello world" ;' ;
echo serialize($payload) ;
// O:6:"Logger":2:{s:8:"filename";s:18:"/var/www/index.php";s:6:"buffer";s:26:"<?php echo "Hello world" ;";}
En injectant cette charge utile dans le cookie, l’application va la
délinéariser et obtenir un objet de type Logger
. Bien sûr,
l’appel à whoAreYou()
va échouer puisque l’objet n’a pas
cette méthode. Mais lorsque le script terminera, l’objet sera détruit,
appelant sa méthode __destruct()
. Celle-ci poussera alors
son tampon dans le fichier sur le serveur, écrivant ce que vous vouliez
là où vous le vouliez.
Cette fois encore, le problème n’est pas dans la classe
Logger
qui ne ferait pas assez attention. Le problème,
c’est que le script a délinéarisé des données provenant d’un attaquant
qui peut donc injecter ce qu’il veut.
Protections et mauvaises idées
C’est maintenant le plus difficile. Je vous ai montré un truc super et alors que vous tombiez amoureux, je vous ai montré qu’en fait, c’est dangereux. Le deuil est difficile et après le choc, le déni puis la colère, vous voulez peut être négocier : « Peut être que si… ».
non, même avec des si, ça reste dangereux. Ça va faire mal, c’est déprimant mais vous finirez par l’accepter et reprendrez le cours normal de votre vie.
Chargement des classes
Pour que l’injection d’objet fonctionne, il faut que les classes des objets que l’attaquant injecte soient chargées par votre application. Vous pourriez donc vous dire :
Mon script charge très peu de classes, je les connais, je ne risque donc rien.
Pour que votre argument soit valable, vous chargez donc manuellement
tous les fichiers nécessaires dans chaque script. Conséquence, vous vous
interdisez scrupuleusement deux fonctionnalités bien pratiques du
PHP
:
- Le chargement automatique des classes et en particulier la norme PSR-4 pourtant très pratiques pour organiser votre code source.
- Les librairies sur packagist ou plus généralement toute notion de dépendance via composer, pourtant bien pratique pour ne pas réinventer la roue.
Notez que pour certains langages, vous ne pouvez pas le désactiver et toute classe dans votre application sera disponible : Java et python, entre autres.
Mais admettons que vous appliquiez ces restrictions.
C’est quand même une mauvaise idée en termes de défense en profondeur car vous avez du définir un périmètre bien trop gros et flou pour être garanti sans problème. Car un logiciel, ça vit : de nouveaux morceaux sont ajoutés régulièrement, d’anciens meurent sans forcément disparaître.
Au fil des maintenances, correctives ou évolutives, le code modifié constitue un risque permanent qu’une classe utile pour un attaquant soit ajoutée. Plus le code évoluera, plus il sera difficile de garantir qu’aucune classe ni méthode ne peut être utilisée par un attaquant. Et admettons que vous trouviez qu’une classe fourni un tel moyen, que feriez vous si elle est en fait nécessaire pour votre application ?
Garder la sérialisation en s’interdisant le chargement automatique des classes et en s’imposant des vérifications formelles de plus en plus coûteuses, c’est comme un chirurgien qui opérerait avec des moufles parce que c’est plus confortable et puis tant pis pour la maniabilité des clamps et des bistouris.
Liste blanche
Depuis PHP 7.0
, la fonction unserialize()
dispose d’un nouveau paramètre avec lequel vous pouvez fournir une liste
blanche des classes autorisées. Si le type de l’objet n’est pas dans la
liste, la délinéarisation échoue en vous fournissant un objet
inutilisable. Et là, je vous vois venir… « Du coup, j’ai juste à
mettre la bonne liste et je peux même utiliser composer ! »
Techniquement, un objet de la classe
__PHP_Incomplete_Class
avec lequel vous ne pouvez presque rien faire. Les accès aux attributs fournissent une valeurNULL
et remontent un logNOTICE
. Les appels aux méthodes échouent en erreur fatale.
Le problème, c’est que même si ça réduit pas mal le risques, ça ne l’élimine pas pour autant. Lors de la maintenance de votre application, qu’est-ce qui vous garanti que personne ne va se tromper ?
- Ajouter la classe qu’il ne fallait pas à la liste…
- Ajouter la méthode qu’il fallait pas à une classe de la liste…
- Ajouter une délinéarisation et oublier la liste…
Cette fois, c’est comme si votre chirurgien vous disait qu’il a compris qu’avec des moufles, les outils sont peu maniables, du coup il opère avec des mitaines, ça laisse quelques petites peluches, mais les coupes sont enfin nettes.
Signer les exports
Sur le même genre de principe qu’en SAML
, on peut
utiliser l’idée de signer les exports. Une fois la donnée
linéarisée, on en calcule la signature cryptographique qu’on joint au
message. Lors de la réception, on vérifie d’abord la signature et si
elle est valide, on délinéarise et on poursuit les calculs.
C’est signé par cryptographie, c’est donc sûr mathématiquement !
Le premier problème, c’est qu’il n’existe aucun standard ni aucune fonctions natives ou intégrées proposant ce mécanisme de linéarisation signée. Il faudrait alors développer sa propre tambouille et je vous rappelle qu’on parle ici de cryptographie. Le domaine pour lequel on conseille justement d’éviter les trucs faits maisons.
Le deuxième, non des moindres, c’est que la signature ne sert pas à ça. Elle permet bien sûr de certifier l’origine des données mais en aucun cas de l’innocuité du contenu. C’est en quelque sorte revenir à une vision périmétrique de la sécurité.
- Soit vous prenez vos données d’un tiers. Quel sont vos garantie sur sa fiabilité ?
- Soit vous signez vos propres données que vous envoyez à vos clients. Quel est le bénéfice par rapport à une session de votre côté ?
- Soit vous signez vos propres données que vous gardez avec vous. Quel est l’intérêt de les signer ?
Après un contrôle sanitaire, le chirurgien s’est enfin décidé à appliquer des mesures strictes. Il se fourni maintenant exclusivement chez un fabricant de mitaines d’origine contrôlée avec certificat d’authenticité sur chaque lot.
Petits projets et preuves de concept
Enfin, j’aimerais terminer sur les petits projets. Ceux qui sont fait très rapidement juste pour tester un truc ou prouver un concept.
C’est juste pour tester vite fait, ça sera jamais en ligne.
En fait, même dans ce cas, je vous déconseille d’utiliser la linéarisation. Je vois que vous êtes déçu et je vais vous dire pourquoi, même pour ces petits codes, c’est une mauvaise idée…
- Si votre projet est si petit, pourquoi avoir besoin d’un truc aussi avancé que la linéarisation ?
- Si vous prenez de mauvaises habitudes pour les petits projets, vous les garderez pour les plus grands.
- Si vous saviez le nombre de preuves de concept qui passent en prod parce que c’est plus rapide et moins cher…
La direction de l’hôpital ayant finalement interdit l’utilisation des moufles et mitaines lors des opérations, le chirurgien les as mis de côté pour l’enseignement et l’entraînement des urgentistes. Une fois au travail, ça leur demande toujours une petite période d’adaptation mais au bout de trois incisions, ça va mieux.
Et maintenant ?
Ne jamais délinéariser
Vous l’aurez donc compris, toute tentative de délinéariser des
données, que ce soit via serialize()
ou des imitations,
n’apportera que ruine et désolation dans votre projet…
- Des coûts de développement croissants pour des vérifications rigoureuses toujours plus complexes,
- Des erreurs d’inattention inévitables, dus à la difficulté, la fatigue, le stresse ou même incompétence,
- Des vulnérabilités difficiles à trouver et surtout à corriger.
Et tout ça parce que vous utilisez une fonction qui, c’est vrai, à l’air sympa, est toute mignonne et viens du temps béni des anciens qui savaient, eux, comment vivre en harmonie avec Le Code.
La sécurité informatique, c’est un métier à risques. Il faut déjà être très bon à l’entraînement pour avoir une chance de pas faire d’erreur en condition réelle. L’excellence est à ce prix : la vigilance permanente.
Se méfier des entrées
Si vous avez vraiment besoin de transférer des structures complexes entre vos applications, mon conseil sera le même que pour toute donnée extérieure à vos composants :
Ne faites pas confiance aux données extérieures.
Les formats de représentation pour émettre et recevoir ne devrait jamais contenir d’instructions de typage qui soit suivi par votre code. Au mieux, vous pourriez joindre des indications qu’il devra vérifier avant de les utiliser.
Exemple avec JSON
Techniquement, n’importe quel format sans typage répond à ce critère.
Vous pourriez vous tourner vers le JSON
qui a l’avantage
d’être très bien intégré par toutes les plateformes.
- La fonction json_encode()
qui permet de transformer n’importe quelle structure en
JSON
. Même si les programmes n’en auront pas besoin, l’optionJSON_PRETTY_PRINT
sera utile si des humains doivent lire ces exports. - La fonction json_decode() qui permet de transformer la chaîne de caractère en tableau associatif que vous pouvez ensuite parcourir pour créer les objets utiles,
- L’interface JsonSerializable qui permet à vos classes de personnaliser l’export. Ce format n’étant pas typé, aucune fonction inverse n’est disponible (et c’est bien là tout l’intérêt).
Pour revenir à notre exemple initial et nos utilisateurs qui se
disent bonjours, on pourrait ajouter une méthode de construction à
partir de JSON
(on peut parler de fabrique
statique) comme suit :
class User implements JsonSerializable {
// Previous code here ...
// Only if name is private
public function jsonSerialize() {
return ["name" => $this->name ] ;
}
public static function FromJson($json) {
$table = json_decode($json) ;
return new User($table["name"])
} }
On peut alors reprendre le code d’exemple de sauvegarde des objets
dans des fichiers en remplaçant la sérialisation par un encodage en
JSON
.
file_put_contents("foo.txt", json_encode(new User("Foo"))) ;
file_put_contents("bar.txt", json_encode(new User("Bar"))) ;
La récupération des objets se fait tout aussi simplement mais cette
fois, on ne laisse pas PHP
choisir le type en fonction du
contenu récupéré, on le force manuellement par l’appel à la fabrique
User::FromJson()
.
$foo = User::FromJson(file_get_contents("foo.txt")) ;
$bar = User::FromJson(file_get_contents("bar.txt")) ;
echo $foo->hello($bar) ;
// Nice to meet you Bar, I am Foo
Les autres exemples se modifient de la même manière et évitent toute injection malveillante.