[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