OVH Cloud OVH Cloud

[Long] Pratiques de codage php et webapps

120 réponses
Avatar
John Gallet
Bonjour,


NB1 : xpost fr.comp.lang.php et fu2 fr.comp.securite (les deux sont
modérés).

Après moult tergiversations, je laisse le fu2 sur fr.comp.securite car
on dépasse le cadre du langage PHP et que les failles à débattre sont
majoritairement
humaines et non liées au langage. J'encourage plus que vivement les
habituels lecteurs/contributeurs de fclphp à suivre la discussion sur
fcs (et à le
consulter régulièrement une fois cette bonne habitude prise ;-)...)

Je me porte volontaire pour essayer de faire une synthèse de la
discussion qu'on pourra intégrer dans l'une des deux FAQs et référencer
depuis l'autre.

NB 2 : Cet article étant long et fcs étant modéré, je rappelle aux
contributeurs qui seraient tentés de le citer intégralement que leur
réponse n'a aucune chance d'être publiée. Si vous avez un doute sur la
bonne manière de répondre sur un forum usenet, usez et abusez de
http://www.giromini.org/usenet-fr/repondre.html


Suite aux divers questions/trolls sur la sécurité des applications
écrites en PHP dans une optique web, je lance un petit débat sur les
pratiques de codage en PHP apportant ou non un vrai "plus" de sécurité.

J'entends par "faille de sécurité" une erreur de codage ou de conception
qui permet de passer outre une procédure d'authentification, d'avoir
accès à des données non publiques, ou de modifier/détruire des
données/des scripts, exclusivement dans une optique web, avec php comme
langage dans mon esrit à l'origine, mais on pourra élargir à d'autres
langages/plateformes de web dynamique comme perl, jsp, asp, .net etc...

Les questions sont les suivantes :

Question 1 :

Quelles sont les principales failles existantes dans les scripts PHP que
vous avez rencontrées ? Quels risques induisaient-elles ? Comment les
avez vous corrigées ?

Question 2 :

Quelles sont les principales fausses vérifications de sécurité que vous
connaissez ? Comment peut-on les contourner (indiquer la difficulté
pour y arriver) ou pourquoi ne sont-elles pas fiables ou non applicables
sur le principe même ?

Question 3 :

Pensez vous à des failles théoriques potentielles que vous n'avez pas
encore vérifiées en pratique ?

------------
Je commence bien entendu :

Question 1 (failles existantes):
a) variables non itilialisées en register_globals=On (injection de
variables)
Risque : principalement accès non autorisés, mais tout est possible.
Correction : initialiser ses variables (Sans blague...), ou utiliser des
fonctions/des objets car ils snt insensibles à l'injection de variables.
Piège : croire qu'on est toujours en register_globals=Off et coder comme
un cochon.
Correction : idem.

b) include dynamiques (ex include($toto);)

Risque : exécution sur sa machine de n'importe quel code souhaité par
l'attaquant (installation de back-doors, défigurations, etc....
Correction : ne pas utiliser d'includes dynamiques ou vérifier que le
fichier est bien local si hébergement dédié. Renforcer les restrictions
d'include_path. Attention, depuis php5, file_exists peut éventuellement
renvoyer TRUE sur des fichiers distants (à restester, je n'ai pas poussé
plus loin que le "tip php5" du manuel).
Piège : essayer de se renforcer avec include($toto.'.php');
Contournement : $toto="[target]/script_sans_extension"; par exemple.

c) injection SQL.
Risque : accès non autorisés, corruption de données
Correction : filtrage des variables, échappement de ' et " par le
caractère ad hoc pour la base de données (\ pour mysql, ' pour sybase
etc...)

d) confiance dans les variables venant de l'extérieur. Par exemple,
recalculer une facture à payer en utilisant un prix transmis par un
champ HIDDEN ou calculé en javascript. Ne pas revalider la donnée parce
qu'elle l'a été en JavaScript.
Risque : multiples. Accès non autorisés, corruption de données, etc...
Correction : ne faire confiance qu'à des données conservées côté serveur
(refaire une requête sgbd pour obtenir le prix de l'article, les frais
de port, etc...). Faire avant tout les validations de cohérence des
données côté serveur et non en javascript.

e) uploads de fichiers.
Outre les failles du langage php lui même qui apparaissent parfois à ce
sujet, les tutoriels que j'ai vus n'insistent pas assez sur le besoin de
faire attention aux extensions autorisées par rapport aux extensions
parsées sur le serveur. Si le serveur considère comme du code php le
fichier toto.php.txt, il faut interdire tout nom de fichier contenant
.php. dans son nom. Ceci doit venir en complément d'une liste
restrictive d'extensions explicitement autorisées (.jpg, .gif, .doc
etc...). Je suis plus particulièrement intéressé sur ce point par les
vérification purement serveur permettant de vérifier le type de fichier
traité.

f) utilisation de header("Location:...)
Algo (erronné)
1. vérification de cohérence
2.1 si problème alors header("Location:bad.php"); // jusqu'ici tout va
bien
2.2 si ok alors header("Location:ok.php"); // et plouf dommage, il
suffit d'appeler directement ok.php avec n'importe quels arguments et
tout passe.
Correction :
2.1 si problème require('erreur.php'); exit();
2.2 (sinon) require('traitement.php'); // rappel : toute variable locale
est alors définie dans traitement.php

g) appels systèmes non filtrés
Dans le même genre que les includes dynamiques, passer directement la
saisie de l'utilisateur à exec() ou system(). Personnellement, j'ai
tendance à interdire tout exécution de code directe, filtrée ou par (je
remplace les actions possibles par des cases à cocher et j'exécute ce
qu'il faut). Peut-être est-ce par trop parano et que 'lon peut autoriser
certaines choses.
Risques : donner la main sur votre machine à un attaquant.
Correction : ne jamais passer quoi que ce soit qui vient de l'extérieur
en argument, mais c'est parfois trop restrictif.

Question 2 (fausses vérifications):

a) vérifier que la donnée a bien été transmise par la méthode POST sous
prétexte qu'elle vient d'un formulaire.
Contournement : il suffit d'envoyer une donnée vérolée par post, que ce
soit en modifiant du html ou en utilisant la librairie CURL par exemple
pour de l'attaque massive. C'est le contenu de la donnée qu'il faut
vérifier, pas son mode de transmission.

b) Vérifier que les données viennent bien "de mon site" en utilisant
HTTP_REFERRER.

Une idée (qui n'est pas de moi) et que je n'ai pas réussi à mettre en
oeuvre : injection SQL par des entiers ou plus généralement injection
SQL insensible aux habituelles vérifications sur les quotes.

Soit la requête : "UPDATE .... WHERE id=$i " avec id de type entier
(typiquement : autoincrement)
But de la manip : injecter dans $i une chaîne transformant la requête en
(par exemple):
"UPDATE ... WHERE id=0 OR 1=1"
(requête qui va corrompre les données en impactant tous les rangs de la
table)
Moyen sous mysql : utiliser la fonction mysql CHAR et complèter la
chaîne en hexa. Mais je n'ai pas réussi à le faire, je me prends ou du
syntax error ou une chaîne non interprêtée.

Espérant faire avancer le shimili... le shcibi... le biniou.

JG

10 réponses

Avatar
Nicob
On Tue, 23 Nov 2004 09:11:46 +0000, John Gallet wrote:

hello, je suis un petit malin : <img
src="...site_externe.../script.php" title="c'est un smiley"> il est
fort probable que dans certaines circonstance un peu facheuses, le
fameux script php soit capable de retrouver des informations
contextuelles propres a pénétrer le site.


Là comme ça, je suis curieux de savoir comment. Côté serveur lors de
la génération de la page d'affichage du forum ou côté client ?


Si l'identifiant de session est inclus dans l'URL (passé via GET), il
apparaitra dans le Referrer du côté des logs du serveur Web du méchant.


Nicob


Avatar
Eric Razny

Question 1 (failles existantes):
a) variables non itilialisées en register_globals=On (injection de
variables)
Piège : croire qu'on est toujours en register_globals=Off et coder comme
un cochon.



Ca c'est une question de gout, et je trouve personnellement que coder
(scripter?) en faisant le postulat register global est off est plus lisible


Coder en faisant le postulat qu'aucune des fonctions facultative de sécu
n'est activée me parait une pratique bien plus sure. Amha faire
réfléchir les développeurs sur le principe de robustesse et leur faire
comprendre qu'ils ne seront pas toujours maître de la manière dont sera
utilisé leur bébé est aussi importante.

Le pire est le développeur/admin/etc qui maitrise tout son
environnement. Le jour ou il doit bosser en groupe (ou tout bonnement
est remplacé -ça peut être parce qu'il trouve mieux ailleurs-) ça pête
souvent en deux temps trois mouvements (genre register_globals "mal"
positionné, changement de base SQL etc)

Moyen sous mysql : utiliser la fonction mysql CHAR et complèter la
chaîne en hexa. Mais je n'ai pas réussi à le faire, je me prends ou du
syntax error ou une chaîne non interprêtée.


Pas besoin de se casser la tete tant qu'on n'a pas besoin de mettre de
quotes car dans 95% des cas, car c'est le seul caractère filtré. Là où
il faut faire attention c'est pour le caractère "=" qui est
avantageusement remplacé par %3D pour éviter que le "truc qui fait la
conversion url->variables" ne croie qu'on a une autre variable.


Et avec d'autres choses plus haut dans le post d'origine :
Amha il *faut* se casser la tête, mais une seule fois pour la majorité
des cas. Ca ne coute pas si cher en temps développement de créer des
fonctions qui parsent les entrées et qui les mettent dans un format
préétabli avec en cas d'écart une transformation ou un refus de traitement.

Bien sur ça va augmenter la charge sur le serveur qui doit être prévu
pour la supporter. Mais il aura plus à supporter s'il est compromis par
la suite :)


Un point important (pour moi au moins) sur le filtrage des variables :
Les magic quotes ne sont pas une fonction de sécurité! Ce n'est pas
parce qu'elles empechent certaines injections sql qu'elles suffisent. Le
cas typique est le mail qui sera utilisé par une autre appli, et qui
meme si il ne contient pas de quotes peut avoir des effets bien plus
graves (pipe par exemple).


Yep.

Le mieux est de filtrer les variables de
manière précise avec des regexp.


Ou plus généralement de vérifier toute entrée (y compris d'un autre
process dont on n'est pas sur, il a très bien pu être mal programmé et
accepter des horreurs :) ) avant d'effectuer des actions potentiellement
dangereuses avec.

Et puisqu'on parle de php, il faut le sécuriser du coté de ses
paramètres, comme le register_global, magic quotes, repertoires où on
peut faire des includes, support des url dans les open ... sécuriser la
BDD est assez important.


Il est important d'activer les options qui favorisent la sécu, mais je
maintiens que si le programmeur ne peut les activer lui même dans son
code (ie seulement les "subir") il ne doit pas compter dessus!

Eric

--
L'invulnérable :
Je ne pense pas etre piratable, infectable par un trojen oui!
Vu sur fcs un jour de mars 2004.


Avatar
John Gallet
Re,

- leur donner une extension ".php" pour que leur contenu ne soit pas
renvoyé vers le client mais interprété par le serveur



Cette partie là ne mange pas de pain de toutes façons.

- n'y inclure que du code faisant partie de fonctions, et surtout pas de
code qui constituerait un main() si le script était appellé directement.
Il me semblerait encore plus fiable d'interdire qu'ils soient servis au

client, ou mieux (si c'est possible avec PHP), les placer en dehors de
l'arborescence publiée.


Oui ce devrait être possible dans tous les cas avec apache, en mettant
ces fichiers dans un sous répertoire protégé par un .htaccess en deny
from all et en les incluant par require('sous_rep/toto.php'); donc même
si on reste dans l'aborescence du serveur http, les fichiers sont
inaccessibles. Ne connaissant pas IIS et autres Zeus ou Caudium ou NES,
je ne sais pas si le mécanisme est reproductible simplement.

En revanche, en général, on interdit à php de faire des include en
"remontant" car après tout, require('/etc/passwd'); ça marche très bien
(si on ne l'interdit pas explicitement)...

a++
JG


Avatar
John Gallet
Re,

hello, je suis un petit malin : <img
src="...site_externe.../script.php" title="c'est un smiley"> il est
fort probable que dans certaines circonstance un peu facheuses, le
fameux script php soit capable de retrouver des informations
contextuelles propres a pénétrer le site.
Si l'identifiant de session est inclus dans l'URL (passé via GET), il


apparaitra dans le Referrer du côté des logs du serveur Web du méchant.


Bien vu. Je n'y aurais pas pensé, je l'avoue, alors que j'en voie
parfois dans mes propres logs. Donc en fait on se fout totalement que ce
soit un script php, toute requête http vers l'extérieur donne, si
exploité durant le temps de vie de la session, la possibilité de voler
la session de l'utilisateur qui regarde la page avec ce lien.

Dans le cadre d'une synthèse de ce thread, qu'est-ce qu'il faut en
conclure ?

- qu'il faut refuser tout lien http externe dès lors qu'on utilise des
sessions ?
- que dans ce cadre là, il faut utiliser des cookies au lieu de passer
l'id par l'url pour être sûr qu'il n'aterrira pas dans un log compromis
?
- qu'il faut coupler avec un jeton de validité très réduite (par exemple
le mécanisme décrit par Marc de validité unique) ?

a++
JG



Avatar
Nicob
On Tue, 23 Nov 2004 11:16:43 +0000, John Gallet wrote:

[...] utiliser les fonctions de la GD-LIB pour vérifier le type de
fichier, mais je ne suis pas allé regarder leur mécanisme (i.e. s'ils
ne font que lire les octets 156 à 187 du fichier, c'est loin d'être
bullet-proof).


Exactement ...

Je viens de créer un fichier PHP valide en rajoutant mon code à la fin
d'une image quelconque. La commande 'file' considère ça comme une image
JPG valide et mon serveur l'exécute sans problème si je lui mets une
extension '.php' :

http://nicob.net/mirrors/blowjob.jpg

Faudrait voir ce qu'en pense la GD-Lib ...


Nicob
NB : Non, ce n'est pas du pr0n ...

Avatar
Patrick Mevzek

Il me semble (ie. c'est ce j'en ai lu sur le Net et mes tests
confirment) que les "requêtes préparées" (disponibles au moins en PHP
et Perl/DBI) sur un MySQL ne sont pas vulnérables au SQL-Injection. En
plus, on peut même avoir un gain en perfs :)


On risque de perdre en portabilité, mais ceci est un autre soucis. En


D'autant que tous les bons SGBDR devraient supporter les requêtes
préparées, c'est que des avantages à la clef :-)

revanche, il me semble avoir lu quelque part dans la couche
d'abstraction adodb (couche php multi-sgbdr disponible ou en .php ou en
extension C) qu'au contraire les perfs avaient tendance à s'en
ressentir.


A priori non, en tout cas pour les requêtes exécutées plusieurs fois avec
des paramètres qui varient.
Car en faisant ainsi le plan d'exécution (recherche des index, choix de la
méthode de jointure, etc...) n'est pas refait à chaque requête mais
conservé. C'est un gain important en performance.

Pour une requête exécutée une seule fois, on n'y gagne rien, et on y perd
peut-être un peu, mais ca doit être négligeable devant le reste.

Exemple :
$sth = $dbh->prepare("INSERT INTO contacts (name,email) VALUES (?,?)");
Si quelqu'un a la preuve (morceau de code) que ce type d'interrogation
de la base protège du SQL-Injection ...


Ca semblerait logique que ça protège (un peu plus seulement ?) si le
SGBDR refuse de parser de nouveau le résultat une fois les placeholders


L'injection SQL, comme beaucoup (toutes ?) d'attaques, se base
profondément sur un problème de changement de contexte. On passe d'une
apostrophe dans le contexte ``chaine de caractères quelconque'' à une
apostrophe dans le contexte SQL, où il y a un rôle spécifique.
Toute fusion de contexte fait naitre une vulnérabilité. En envoyant au
SGBDR d'une part la requête sans les ``variables'', d'autre part les
variables, il n'y a plus de fusion de contexte, et le SGBDR n'a pas besoin
de parser quoi que ce soit.

Utilisation intéressante, je n'y avais jamais pensé. Mais qu'est-ce que
c'est chiant à coder...


Ca me parait au contraire particulièrement élégant. On peut englober la
requête stockée (avant son exécution) dans un objet, voire la passer de
fonctions en fonctions.

--
Patrick Mevzek . . . . . . Dot and Co (Paris, France)
<http://www.dotandco.net/> <http://www.dotandco.com/>
Dépêches sur le nommage <news://news.dotandco.net/dotandco.info.news>


Avatar
Cedric Blancher
Le Tue, 23 Nov 2004 15:36:49 +0000, John Gallet a écrit :
- qu'il faut refuser tout lien http externe dès lors qu'on utilise des
sessions ?


Cela n'empêchera pas l'utilisateur d'entrer une URL à la main et au
navigateur de se sentir obligé de remplir le Referer quand même.

- que dans ce cadre là, il faut utiliser des cookies au lieu de passer
l'id par l'url pour être sûr qu'il n'aterrira pas dans un log
compromis ?


On ne peut pas se baser sur ce postulat qui réduirait l'accessibilité au
site pour les navigateurs (antiques) ne supportant pas les cookies ou
configurés par leurs utilisateurs (paranoïaques) pour ne pas les
utiliser.

- qu'il faut coupler avec un jeton de validité très réduite (par
exemple le mécanisme décrit par Marc de validité unique) ?


Ça me semble être la meilleure solution. C'est lourd à gérer, mais, il
faut savoir ce qu'on veut. Une seconde contrainte importante (citée par
Marc) sur ce jeton me semble être l'obfuscation maximum de ses éléments
générateurs, à base de hash par exemple, pour éviter qu'on puisse
prédire la valeur du jeton n+1 à partir du jeton n (ou autre info
pertinente).


--
BOFH excuse #276:

U.S. Postal Service

Avatar
Patrick Mevzek
Si l'identifiant de session est inclus dans l'URL (passé via GET), il
apparaitra dans le Referrer du côté des logs du serveur Web du méchant.


Bien vu. Je n'y aurais pas pensé, je l'avoue, alors que j'en voie
parfois dans mes propres logs. Donc en fait on se fout totalement que
ce


(et pour la même raison, il ne faut pas loguer des mots de passe, car si
les gens se trompent d'endroit, et mettent le mot de passe X au lieu de
mettre le Y - pour ceux qui ont le bon gout d'en avoir plusieurs -, eh
bien on peut récupérer dans des logs des mots de passe valide)

soit un script php, toute requête http vers l'extérieur donne, si
exploité durant le temps de vie de la session, la possibilité de voler
la session de l'utilisateur qui regarde la page avec ce lien.


Oui.

Dans le cadre d'une synthèse de ce thread, qu'est-ce qu'il faut en
conclure ?


Faire des liens externes qui repointent vers l'application, sans
identifiant dans l'URL, laquelle application générera une redirection.
Le navigateur utilisera donc l'URL externe avec comme Referer (s'il en
met un d'ailleurs, et s'il n'est pas filtré par un proxy) l'URL
précédente dans l'application qui n'avait pas d'identifiant à l'intérieur.

--
Patrick Mevzek . . . . . . Dot and Co (Paris, France)
<http://www.dotandco.net/> <http://www.dotandco.com/>
Dépêches sur le nommage <news://news.dotandco.net/dotandco.info.news>


Avatar
Nicob
On Tue, 23 Nov 2004 15:46:59 +0000, Cedric Blancher wrote:

- qu'il faut coupler avec un jeton de validité très réduite (par
exemple le mécanisme décrit par Marc de validité unique) ?


Ça me semble être la meilleure solution. C'est lourd à gérer, mais, il
faut savoir ce qu'on veut.


Il existe une technique tout bête permettant d'empêcher l'utilisation
d'un identifiant de session volé par XSS ou Referrer : dériver cet
identifiant de l'adresse IP du client, et rendre le cookie inutilisable si
pas utilisé depuis la bonne IP.

Evidemment, on emploiera une fonction de hash sur notre n-tuple
authentifiant l'utilisateur avant de s'en servir comme identifiant. Les
deux papiers listés ci-après, bien que vieillissants, restent une très
bonne base théorique quant à ces problèmes d'identifiant de session ...

Dos and Don'ts of client authentification on the Web :
http://www.cs.cornell.edu/People/egs/syslunch-spring02/syslunchsp02/webauth_tr.pdf

Secure cookies on the web :
http://www.list.gmu.edu/infs767/home/journals/ic/pdf_ver/ieeeic00.pdf


Nicob


Avatar
Laurent Seguin
Nicob , le 23 nov. 2004 16:36:49, écrivait ceci:

[...] utiliser les fonctions de la GD-LIB pour vérifier le type de
fichier, mais je ne suis pas allé regarder leur mécanisme (i.e. s'ils
ne font que lire les octets 156 à 187 du fichier, c'est loin d'être
bullet-proof).


Je viens de créer un fichier PHP valide en rajoutant mon code à la fin
d'une image quelconque. La commande 'file' considère ça comme une image
JPG valide et mon serveur l'exécute sans problème si je lui mets une
extension '.php' :
http://nicob.net/mirrors/blowjob.jpg

Faudrait voir ce qu'en pense la GD-Lib ...


Ce n'était pas avec la GD lib mais avec le type mime retourné par
getimagesize. J'ai essayé et il donne bien un type image/jpeg.
Niké mes vérifs d'upload d'image :-/...