[cours-linux-esnig] Commentaires sur le projet
Marc SCHAEFER
schaefer at alphanet.ch
Thu Jul 10 10:00:07 CEST 2003
Bonjour,
après avoir testé, lu la documentation et vu vos sources pour le projet,
j'ai constaté que le code est tout à fait correct dans la grande partie,
que les fonctionnalités sont présentes et que des efforts sensibles
pour la sécurité ont été consentis.
Malgré tout, quelques critiques générales dans le but d'améliorer la
qualité:
HTML
- Il est important de comprendre la différence entre code PHP et
code HTML. En particulier, on vous demandait de valider, via
http://validator.w3.org/, le *code HTML* _généré_ par les scripts
PHP. En bref, cela signifie que si votre serveur WWW est accessible
depuis Internet, vous entrez simplement l'URL de votre script PHP
exécutable
Sinon vous accédez à vos scripts accédez via un client WWW (w3m, Mozilla,
Konqueror, etc) et vous sauvez le document retourné en HTML.
C'est ce fichier que vous pouvez transférer via Validate file. Et
non pas un fichier contenant du code PHP.
- Il est important d'avoir un <!DOCTYPE au début du document
(c'est nécessaire pour respecter le standard et cela simplifie
la validation).
- Si l'on force le type d'HTML (DOCTYPE) à HTML 4.01/Transitional
(ou Frameset), la plupart des fichiers HTML passent. Respecter ce
standard garantit un minimum d'interopérabilité.
- Le code Javascript n'est pas contrôlé par le validateur HTML, il
peut donc être non standard. D'ailleurs je ne connais pas de
validateur Javascript.
J'ajoute que le Javascript peut être utilisé pour valider les données
_dans la mesure où cette validation est répétée sur le serveur dans le
script PHP_. Sinon cela s'appelle `user-side-security' et cela ne
sert à rien. Par contre, l'avantage de la validation également en
Javascript est de décharger le serveur et d'implémenter une meilleure
interactivité/réaction aux erreurs de l'utilisateur. Mais aucun site
ne devrait en dépendre, en particulier pour la sécurité.
- Erreurs typiques:
- Structure du HTML:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<HTML>
<HEAD>
<TITLE>...</TITLE>
</HEAD>
<BODY>
</BODY>
</HTML>
Exemple: faire un `view source' sur http://www.cril.ch/,
sélectionner le lien de validation.
- Oublier de terminer certains tags:
<a href="...">...</a>
<select>...<option>...</option>...</select>
- Oublier de spécifier les listes:
<ul>
<li>
<li>
</ul>
(</li> est optionnel)
- <br> n'a pas de tag de fermeture.
Qualité
- Un code de qualité doit être compréhensible, réutilisable et
paramétrable.
Mettre les fonctions souvent utilisées dans un fichier
d'include (cela a souvent été fait). En particulier les
mots de passe!
En PHP:
include("include/password.inc");
include("include/fonctions.inc");
NB: regardez ce qu'il se passe si vous demandez
http://votre-site/projet/include/password.inc. Si vous
voyez vos mots de passe, protégez ce répertoire dans la
configuration Apache ou appelez le fichier .php, ce qui
devrait le rendre interprétable mais non visible.
Peut-être séparer le traitement de l'affichage HTML:
- vérification des paramètres (filtrage, sécurité)
- requêtes de base de données
- affichage
(en trois fonctions ou séquences d'instructions clairement
séparées).
Peut-être développer une fonction d'affichage de table générique
(certains l'ont fait).
Mettre des commentaires directement dans le code.
- Peut-être qu'il faut éviter certains artifices, comme les REFRESH
(le standard HTML 4 les interdit). Une stratégie intéressante peut
être d'avoir toute la fonctionnalité dans un seul script
(biblio.php3) et d'ensuite implémenter les fonctions réelles via
des fichiers include: ainsi l'utilisateur ne doit pas jongler
avec des boutons back/forward, il revient immédiatement au bon
endroit.
- Certains ont fait de réels efforts de design! J'ai tendance à penser
que (surtout si l'on utilise un programme souvent), on préfère
avoir un système fiable et simple plutôt qu'une interface trop
design, mais un design efficace et agréable peut aussi rendre les
choses bien plus efficaces, en particulier s'il repose sur une
base simple et solide. En particulier les CSS permettent -- sur les
navigateurs pas trop anciens -- de séparer structure et présentation
et de rendre la présentation facilement modifiable.
- Ce n'est pas très beau d'avoir des `warnings' ou des erreurs
difficilement identifiables. Ajouter des tests d'erreur circonstanciés,
ce qui peut simplifier le debug à l'installation ou en cas de problème.
$db = $mysql_connect("test", "abcd");
// ici tester $db
De même pour mysql_select_db(), mysql_query(), mysql_fetch_array().
Ne pas hésiter à lire les documentations PHP de ces fonctions.
- Une idée peut être d'avoir une variable globale $debug, normalement à
zéro (dans un fichier d'include), et qui peut être activée et
ajouter des messages de debug:
message_si_debug()
la fonction n'affiche quelque chose que si $debug est à 1.
Certaines personnes ont utilisé cette idée.
- La question de savoir si des GET ou des POSTs sont mieux m'a
été posée. Dans le premier cas, les variables (paramètres) sont
visibles dans l'URL: <a href="script.pl?var1=truc&var2=abcd">...</a>.
Dans le deuxième ils sont invisibles, et ne peuvent être cachés ou
`bookmarkés'.
Tout dépend ce que l'on fait: pour les fonctions de bases, je dirais
que c'est un avantage de pouvoir `bookmarker' et faire `back/forward'
sans que les données n'expirent dans le cache à cause d'un POST.
Par contre, pour le cas où l'on a des valeurs temporaires ou une
succession de questions avec un contexte, un POST est mieux.
On peut aussi sauver les variables dans un cookie, ou dans un
paramètre, et associer la valeur du cookie à une structure de données
d'état complexe dans la base de données: ce qui rend impossible
les problèmes de navigation ou de `submit' multiple vu que le contexte
est sauvé sur le serveur. Le cookie peut être invalidé
après un logout ou un délai dépassé.
- Casse: en règle générale, utiliser des majuscules uniquement et
utiliser le souligné '_' comme séparateur. En effet, les noms de tables
et d'identificateurs peuvent être parfois significatifs (MySQL sous
UNIX), et les noms de fichiers -- donc les noms de scripts également
aussi. Faire ainsi évitera des problèmes de portabilité, en particulier
de Windows ou MacOS à UNIX.
Documentation
- Il peut être intéressant de documenter le schéma de la base de
données: certains l'ont fait. Il semble même que des outils existent
pour convertir un modèle EA écrit sous l'éditeur DIA directement
en SQL exploitable.
- Si l'on ne fournit pas d'installation automatisée, et même si,
documenter celle-ci peut être une bonne idée.
- Documenter la liste des fichiers et ce qu'ils font est un strict
minimum (fichier README, créé par un `ls -1' puis manuellement
en ajoutant des commentaires).
- Idéalement, lorsqu'on archive un projet (pour le transmettre ou
comme sauvegarde), il faudrait archiver le répertoire. De manière à
ce que le désarchivage soit facile.
Par exemple:
cd ..
tar cf - nom_projet | gzip -9 > nom_projet.tar.gz
Aussi, faire attention aux fins de ligne MS-DOS dans les fichiers texte.
Dans certains cas, cela peut poser des problèmes sous UNIX.
sed 's/^V^M//g' < fichier.txt > fichier
(^V et ^M signifient Control-V et Control-M). Certaines distributions
ont un outil dos2unix pour le faire. Dans vi, ces fichiers apparaissent
comme [dos mode].
Fonctionnalités
- Dans l'ensemble, les fonctionnalités demandées ont été implémentées.
Parfois on est allé plus loin.
- Parfois, malheureusement, tout n'a pas été implémenté ou des fichiers
manquaient (j'ai contacté les personnes dans l'espoir de corriger
cela). C'est un peu dommage d'avoir des fonctionnalités non demandées
présentes sans toutes les fonctionnalités demandées.
Sécurité
Il y a un cours sécurité l'année prochaine à l'ESNIG, mais je vais
essayer de faire un résumé de 150 périodes en quelques lignes :))
1. introduction/altération/consultation de données non autorisées
Dans un premier lieu, on pensera à l'authentification (par
mot de passe, .htaccess avec Apache, ou par cookie p.ex.).
Mais ce n'est pas tout.
Cette classe contient également les attaques de
cross-scripting: la base de données contient un champ `nom'
qui contient '<img src="http://...." alt=""></img>. Visualiser
une table contenant ce champ, p.ex., fera activer le lien
image sans poser de question à l'utilisateur. Si celui-ci s'est
identifié sur le site concerné (cookie, BASIC-AUTH), le premier
script (ou tout utilisateur pouvant insérer des données dans la
DB) peut abuser cette authentification et altérer des données
p.ex.
Solution: passer à la moulinette (htmlspecialchars()) avant
d'envoyer au client WWW toute chaîne provenant de
données pouvant être contrôlées par l'utilisateur
(au minimum: champs de formulaires, données DB).
Schéma: (se lit mieux avec une fonte non proportionnelle!)
|register_global = off
|(évite attaque sur variables
|de script, utiliser $_POST[])
|
| |éviter cross-scripting
| |en n'envoyant en HTML
| |que des données via
| |htmlspecialchars()
| |
FORM ----> script ----> DB ----> script ----> HTML
|
|éviter attaque SQL insertion via
|magic_quote = On, ou, mieux,
|addslashes() au bon endroit suivant la
|DB.
(une FORM entre les données dans la DB via le script,
et plus tard le script génère du HTML depuis les données
stockées dans la DB)
Attention: si magic_quote n'est pas à off, PHP rajoute des
backslashes dans les FORMs et ce n'est pas le modèle correct.
Une solution robuste de scripts accédant à une base de données
pourrait:
- limiter la lecture à l'utilisateur MySQL usuel
- les scripts d'administration utilisent un autre utilisateur
qui a accès lecture/écriture, mais sont protégés p.ex. par
un .htaccess
- éventuellement utiliser des mécanismes de la base de données
(contraintes d'intégrité, triggers) pour éviter de modifier
certaines données (-> plutôt PostgreSQL que MySQL).
Exemple: un salaire ne peut être négatif, la somme des
salaires ne peut excéder le budget, une écriture
de comptabilité ne peut être qu'extournée, pas
effacée.
- en cas de serveur partagé utiliser le safe_mode PHP ou le
mode CGI suexec.
2. introduction/altération de données contradictoires/incorrectes
Voir la dernière partie du 1.
4. abus des scripts pour passer outre les sécurités du script,
de la base de données, ou du système Linux.
$sql = " SELECT * from livres, ";
$sql = $sql . "auteurs where livres.auteurid=auteurs.auteurid and";
$sql = $sql . " (auteurs.nom=\"$Auteur\" or livres.titre=\"$Titre\")";
Premièrement, $Auteur n'est pas accédé via le tableau
$_POST[]
ce qui signifie que l'espace des variables du script et l'espace
des variables sous contrôle de l'utilisateur du script via WWW
est le même. Rappelons que seulement si register_globals=off est
configuré, il est impossible de manipuler les variables du script
de l'extérieur (et $var ne marche plus pour accéder
$_POST[var]). Cf le php.ini.
Deuxièmement, si $Auteur contient des guillemets, il y aura
erreur.
Finalement, si $Auteur contient '"; DROP DATABASE biblio;
SELECT * FROM LIVRES WHERE (auteur.nom="', il est possible
d'abuser le script en ajoutant des ordres SQL (on appelle
cela: `SQL INSERTION ATTACK').
Solution:
- mettre tous les magic_* à `off'
- utiliser addslashes(): avec la plupart des DB, rendra
les chaînes inoffensives.
Autre solution:
- Le `binding' de Perl.
Exemple: (pseudo-code)
$query = 'SELECT * FROM utilisateurs WHERE (nom LIKE ?)'
do_query($query, [ $nom ]) or die("error");
(on lie les variables dans la chaîne avec ? au lieu de
faire du traitement de texte. La fonction do_query() passe
les paramètres directement à la DB, en `binaire'
(structures de données langage C), ou s'occupe de
faire un `addslashes' approprié. Cf man DBI.
5. erreurs non communiquées à l'utilisateur qui croit que tout
marche parfaitement.
Exemple typique:
$result = mysql_query($query);
if ( !$result ) {
... traitement d'erreur
}
else {
... traitement normal
}
6. atomicité
Ce problème est assez ennuyeux. Il s'agit par exemple de déterminer
si un produit commandé existe réellement. MySQL, du moins jusqu'à
la version 4 non comprises, n'avait aucun concept de transaction ni
d'intégrité référentielle correcte.
D'autres bases de données relationnelles, je pense à PostgreSQL
implémentent:
- les contraintes référentielles
(on ne peut insérer une référence à un produit dans ligne
de commandes que si ce produit existe; on ne peut supprimer
un produit que si aucune ligne de commandes ne le référence)
- les transactions
Dans notre cas l'intégrité référentielle suffirait: on ferait
l'insert dans ligne_de_commandes et la base de données nous dirait
si le produit n'existe pas.
En conclusion:
Même dans un cas aussi simple que notre système de commande, on
se rend vite compte des limites de bases de données simples
comme MySQL qui forcent à effectuer les contraintes d'intégrité
dans le code de script (p.ex. PHP) et ne garantissent pas grand
chose en cas d'exécution concurrente.
Voilà! Je vous remercie et j'espère que je ne vous ai pas trop fait peur!
Le but d'un tel projet est d'apprendre, et je crois que cela a été le cas.
Je reste à disposition pour des questions en public (sur la liste) ou en privé.
Bonne continuation et bonnes vacances pour ceux qui en prennent
maintenant!
More information about the cours-linux-esnig
mailing list