This commit is contained in:
Francois Lesueur 2022-03-24 08:31:23 +01:00
parent 020a7b9883
commit 35d3ab5f34
18 changed files with 509 additions and 0 deletions

139
tp5-bd.md Normal file
View File

@ -0,0 +1,139 @@
# TP 5 : Sécurité des bases de données (12h)
Pour ce TP, nous allons utiliser Docker. Vous pouvez l'installer sur votre machine ou dans une VM quelconque.
Ce TP peut être réalisé dans la VM MI-LXC disponible [ici](https://flesueur.irisa.fr/mi-lxc/images/milxc-debian-amd64-1.4.1.ova) (identique à la première période). Avant de lancer la VM, il peut être nécessaire de diminuer la RAM allouée. Par défaut, la VM a 3GO : si vous avez 4GO sur votre machine physique, il vaut mieux diminuer à 2GO, voire 1.5GO pour la VM (la VM devrait fonctionner de manière correcte toujours).
Quelques commandes utiles
=========================
Dans la console psql :
* \l lister les bases
* \dt lister les tables
* \du (\du+) lister les rôles (utilisateurs/groupes)
* \z lister les permissions
* TABLE clients; afficher la table clients
* \c database; ouvrir la base database
* La doc de référence : [doc](https://www.postgresql.org/docs/current/index.html)
Démarrage (30 minutes)
=========
Ici, on va démarrer une base PostgreSQL et faire de premières manipulations. Pour créer et démarrer un docker postgresql :
`docker run --name mypostgres -e POSTGRES_PASSWORD=foo -d postgres`
Ensuite, pour se connecter à la console :
`docker exec -it mypostgres psql -U postgres`
La base est persistente si vous stoppez/redémarrez le docker, mais pas en cas de fausse manip. Prenez des notes au fur et à mesure !
Création d'une première base (2 heures)
============================
Il faut tout d'abord comprendre la notion de rôles (utilisateurs/groupes) et de bases.
Rôles
-----
La documentation est à lire [ici](https://www.postgresql.org/docs/current/user-manag.html) (jusque 22.2 seulement pour l'instant). Les groupes et les utilisateurs sont des rôles avec des attributs différents. La modification d'un rôle est expliquée [ici](https://www.postgresql.org/docs/current/sql-alterrole.html).
> Créez un utilisateur ayant le droit de se connecter, avec un mot de passe. Puis utilisez ALTER pour changer son mot de passe.
Base
----
Une base est un ensemble de tables (relations) qui appartient à un utilisateur. La création de base est documentée [ici](https://www.postgresql.org/docs/current/managing-databases.html) (ignorez les parties templates et tablespaces).
> Créez une nouvelle base qui doit appartenir à l'utilisateur que vous avez créé. Puis supprimez-là, récréez-là et utilisez ALTER pour modifier (puis remettre) son propriétaire.
Tables
------
Les tables sont les éléments des bases qui vont contenir les données qui seront requêtées. Pour créer une table, la [doc](https://www.postgresql.org/docs/current/ddl-basics.html). Il faut ensuite la remplir avec [INSERT/UPDATE/DELETE](https://www.postgresql.org/docs/current/dml.html) et la requêter avec [SELECT](https://www.postgresql.org/docs/current/queries.html).
> Créez une table, insérez et modifiez quelques lignes et requêtez-là avec un WHERE simple.
Nous avons maintenant le nécessaire pour explorer les fonctionnalités de sécurité.
Disponibilité (30 minutes)
=============
Expérimentez le dump et la restauration tel que décrit [ici](https://www.postgresql.org/docs/current/backup-dump.html)
Pour vous connecter en shell sur le docker, `docker exec -it mypostgres bash` (où bash peut évidemment être remplacé par toute commande que vous vouliez taper dans le docker).
> Présentez votre déroulé.
Contrôle d'accès (3 heures)
================
Pour la confidentialité et l'intégrité, nous allons voir la gestion des comptes et des droits.
<< PAUSE COURS AU TABLEAU >>
Gestion des comptes
-------------------
Il est maintenant temps de lire la gestion de l'appartenance aux rôles [ici](https://www.postgresql.org/docs/current/role-membership.html).
> Créez quelques utilisateurs, quelques groupes et affectez des utilisateurs aux groupes.
Gestion des droits
------------------
PostgreSQL permet de régler les droits :
* des tables : [ici](https://www.postgresql.org/docs/current/ddl-priv.html)
* des lignes : [ici](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
La gestion par tables permet la première approche à grain moyen, la gestion par lignes permet ensuite une granularité de contrôle beaucoup plus fine. Les droits sont affectés à des rôles afin de faire du RBAC.
> Sur une table, permettez à un rôle de lire uniquement (SELECT) et à un autre d'écrire.
> Sur des lignes, limitez la lecture à l'utilisateur nommé sur la ligne.
Mise en œuvre (6 heures)
=============
Bravo ! Vous voilà maintenant responsable de mettre une formidable application en production ! Cette application a été développée avec les dernières technologies à la mode par le stagiaire qui vient de partir (parce que c'était, avant vous, le seul technique de l'organisation).
Cette application est disponible dans le sous-dossier [tp5-files](tp5-files/). Vous pouvez la lancer avec `docker-compose up -d` (en étant placé dans le sous-dossier contenant le docker-compose.yml) et y accéder depuis votre hôte aux URLs `http://localhost/admin/`, `http://localhost/clients/` et `http://localhost/helpdesk/`.
Vous pouvez explorer son code (et son README.txt) dans le sous-dossier [webapp](tp5-files/webapp/). La base initiale est décrite dans [sqlinit.sql](tp5-files/sqlinit/sqlinit.sql), ce qui vous permet de connaître les authentifiants attendus. Pour vous connecter à la base (credentials www/foo) :
* `docker exec -it postgres psql -U www clientsdb`
* ou via un navigateur vers `http://localhost:81` (phppgadmin)
Et là, c'est le drame. En regardant `clients/do_login.php`, vous prenez peur pour la mise en production.
> Quel est le problème dans ce fichier ? Retrouvez-vous ce problème ailleurs ? Qu'aurait-il fallu faire à la place ? Exploitez-le !
Malheureusement, la recette a déjà eu lieu et vous n'avez plus la possibilité de faire des modifications au travers de toute l'application. Nous allons explorer 2 voies de défense en profondeur afin de limiter les impacts :
* Séparation des accès à la BD en 3 utilisateurs distincts admin/helpdesk/client puis restriction des droits sur les tables
* Séparation des accès à la BD en n utilisateurs distincts appartenant à l'un des 3 rôles puis application de Row-Level security
Segmentation des accès en 3 utilisateurs
----------------------------------------
Raffinez l'authentification entre l'application et la BD afin de limiter les dégâts potentiels :
* Créez 3 utilisateurs distincts admin/helpdesk/client au niveau de la BD, qui correspondront aux usages des 3 sous-dossiers de l'application
* Attribuez leur les droits minimaux nécessaires à chacune des sections de l'application (droits de SELECT, INSERT et UPDATE sur la table clients)
* Modifiez l'inclusion du `db.inc.php` pour que les fichiers PHP de chaque sous-dossier se connectent à la base avec les credentials adaptés
> Déployez ce modèle et mettez à jour le code PHP en fonction. Vérifiez que certaines exploitations initiales ne fonctionnent plus.
Segmentation des accès en n utilisateurs
----------------------------------------
Raffinez l'authentification entre l'application et la BD afin de limiter les dégâts potentiels :
* Un utilisateur de l'application web = un utilisateur de la BD
* L'authentification sera réalisée directement avec la BD (stockages des authentifiants dans la session PHP)
* Des rôles (hiérarchiques) pour factoriser la gestion des droits
* Une sécurité à grain fin au niveau des lignes de tables (Row security)
> Proposez (sur papier) et faîtes valider un modèle RBAC adapté. Déployez ce modèle et mettez à jour le code PHP en fonction. Vérifiez que votre exploitation initiale ne fonctionne plus.
> REMARQUE : En l'absence de row-level security (pas disponible avec MySQL/MariaDB par exemple), un résultat relativement similaire (mais plus complexe à maintenir) aurait pu être obtenu avec l'utilisation de vues.

View File

@ -0,0 +1,37 @@
version: "3.3"
services:
db:
image: postgres
container_name: postgres
#networks:
# - postgres
environment:
- POSTGRES_DB=clientsdb
- POSTGRES_PASSWORD=foo
- POSTGRES_USER=www
ports:
- 5432:5432
volumes:
- ${PWD}/sqlinit:/docker-entrypoint-initdb.d
- mydb:/var/lib/postgresql/data
webapp:
build: webapp/
image: webapp-image
container_name: webapp
ports:
- 80:80
volumes:
- ${PWD}/webapp:/var/www/html
phppgadmin:
image: bitnami/phppgadmin
container_name: phppgadmin
ports:
- 81:8080
environment:
- DATABASE_HOST=postgres
volumes:
mydb:

View File

@ -0,0 +1,11 @@
-- CREATE USER www WITH PASSWORD 'foo';
-- CREATE DATABASE clientsdb OWNER www;
\c clientsdb
CREATE TABLE clients (id SERIAL, name varchar(15), password varchar(15), role varchar(15), email varchar(25), comment varchar(5000), message varchar(5000));
ALTER TABLE clients OWNER TO www;
INSERT INTO clients( name, password, role) VALUES ('admin', 'admin', 'admin');
INSERT INTO clients( name, password, role) VALUES ('helpdesk1', 'helpdesk1', 'helpdesk');
INSERT INTO clients( name, password, role) VALUES ('helpdesk2', 'helpdesk2', 'helpdesk');
INSERT INTO clients( name, password, role) VALUES ('client1', 'client1', 'client');
INSERT INTO clients( name, password, role) VALUES ('client2', 'client2', 'client');
INSERT INTO clients( name, password, role) VALUES ('client3', 'client3', 'client');

3
tp5-files/webapp/Dockerfile Executable file
View File

@ -0,0 +1,3 @@
FROM php:apache
RUN apt-get update && apt-get install -y libpq-dev && docker-php-ext-install pgsql

13
tp5-files/webapp/README.txt Executable file
View File

@ -0,0 +1,13 @@
Application web de gestion clientèle.
- admin/ contient le code pour ajouter/supprimer des clients
- helpdesk/ contient le code pour modifier des clients, lire leur message et le modifier
- clients/ contient le code permettant à chaque client de voir ses infos et d'envoyer un message au helpdesk
Dans chaque dossier :
- index.php fournit la page de login
- do_login.php traite le login, stocke login et pass dans la session PHP puis redirige vers la page d'affichage print_clients.php
- print_clients.php affiche, selon le cas, la liste + ajout/suppression, la liste + modification ou le compte du client connecté
- do_*.php traite les actions sur la BD (insertion, suppression, modification) demandées par print_clients.php puis redirige vers print_clients.php
La table utilisée est 'clients' de la base 'clientsdb' avec le compte www/foo.

View File

@ -0,0 +1,19 @@
<?php
ob_start();
session_start();
$_SESSION['login']=$_POST['login'];
$_SESSION['password']=$_POST['password'];
require_once("../db.inc.php");
$login = $_POST['login'];
$password = $_POST['password'];
$query = "SELECT * FROM clients WHERE name='$login' AND password='$password' AND role='admin'";
$result = pg_query($dbconn, $query) or die('Échec de la requête : ' . pg_last_error());
if (pg_num_rows($result) > 0)
header('Location: print_clients.php');
else
print "Authentication failed, $query";
?>

View File

@ -0,0 +1,32 @@
<?php
ob_start();
session_start();
echo "Login is ". $_SESSION['login'];
echo "&nbsp;<a href=login.php?logout=1>Logout</a><br/><br/>";
require_once("../db.inc.php");
// Gestion des ajouts/suppressions
if (isset($_POST['name'])) { // Ajout d'un nouveau client
$name=$_POST['name'];
$email=$_POST['email'];
$comment=$_POST['comment'];
$password=$_POST['password'];
$role=$_POST['role'];
$query = "INSERT INTO clients (name, email, comment, password, role) VALUES ('$name','$email','$comment', '$password', '$role')";
pg_query($dbconn, $query) or die('Échec de la requête : ' . pg_last_error());
}
if (isset($_GET['delete'])) { // Suppression d'un client
$id = $_GET['delete'];
$query = "DELETE FROM clients WHERE id='$id'";
pg_query($dbconn, $query) or die('Échec de la requête : ' . pg_last_error());
}
// Ferme la connexion
pg_close($dbconn);
header('Location: print_clients.php');
?>

View File

@ -0,0 +1,17 @@
<?
session_start();
unset($_SESSION['login']);
unset($_SESSION['password']);
?>
<html><head><title>Login Admin</title></head>
<body>
<form action="do_login.php" method="post">
Login: <input type="text" name="login"><br/>
Password: <input type="password" name="password"><br/>
<input type="submit" value="Submit">
</form>
</body>
</html>

View File

@ -0,0 +1,45 @@
<?php
session_start();
echo "Login is ". $_SESSION['login'];
echo "&nbsp;<a href=index.php?logout=1>Logout</a><br/><br/>";
require_once("../db.inc.php");
// Affichage de la table des clients
// Exécution de la requête SQL
$query = 'SELECT * FROM clients';
$result = pg_query($dbconn, $query) or die('Échec de la requête : ' . pg_last_error());
// Affichage des résultats en HTML
echo "<table border=1><tr><td>Name</td><td>Email</td><td>Comment</td><td>Role</td><td>Supprimer</td>\n";
while ($line = pg_fetch_array($result, null, PGSQL_ASSOC)) {
$id=$line['id'];
$name=$line['name'];
$email=$line['email'];
$comment=$line['comment'];
$role=$line['role'];
echo "\t<tr><td>$name</td><td>$email</td><td>$comment</td><td>$role</td>\n";
echo "\t<td><a href=do_support.php?delete=".$id.">Supprimer</a></td></tr>\n";
}
echo "</table>\n";
// Libère le résultat
pg_free_result($result);
// Ferme la connexion
pg_close($dbconn);
?>
<br/><br/>
Ajouter un client :<br/>
<form action=do_support.php method=post>
Name: <input type="text" name="name"><br/>
Password: <input type="text" name="password"><br/>
Role: <input type="text" name="role"><br/>
Email: <input type="text" name="email"><br/>
Comment: <input type="text" name="comment"><br/>
<input type="submit" value="Submit">
</form>

View File

@ -0,0 +1,19 @@
<?php
ob_start();
session_start();
$_SESSION['login']=$_POST['login'];
$_SESSION['password']=$_POST['password'];
require_once("../db.inc.php");
$login = $_POST['login'];
$password = $_POST['password'];
$query = "SELECT * FROM clients WHERE name='$login' AND password='$password' AND role='client'";
$result = pg_query($dbconn, $query) or die('Échec de la requête : ' . pg_last_error());
if (pg_num_rows($result) > 0)
header('Location: print_clients.php');
else
print "Authentication failed, $query";
?>

View File

@ -0,0 +1,19 @@
<?php
// ob_start();
session_start();
require_once("../db.inc.php");
// Gestion des modifications
if (isset($_POST['message'])) { // Ajout du message dans la BD
$message=$_POST['message'];
$login=$_SESSION['login'];
$query = "UPDATE clients SET message='$message' WHERE name='$login'";
pg_query($dbconn, $query) or die('Échec de la requête : ' . pg_last_error());
}
// Ferme la connexion
pg_close($dbconn);
header('Location: print_clients.php');
?>

View File

@ -0,0 +1,17 @@
<?
session_start();
unset($_SESSION['login']);
unset($_SESSION['password']);
?>
<html><head><title>Login Client</title></head>
<body>
<form action="do_login.php" method="post">
Login: <input type="text" name="login"><br/>
Password: <input type="password" name="password"><br/>
<input type="submit" value="Submit">
</form>
</body>
</html>

View File

@ -0,0 +1,37 @@
<?php
session_start();
echo "Login is ". $_SESSION['login'];
echo "&nbsp;<a href=index.php?logout=1>Logout</a><br/><br/>";
require_once("../db.inc.php");
// Affichage de la table des clients
// Exécution de la requête SQL
$login = $_SESSION['login'];
$query = "SELECT * FROM clients WHERE name='$login'";
$result = pg_query($dbconn,$query) or die('Échec de la requête : ' . pg_last_error());
// Affichage des résultats en HTML
echo "<table border=1><tr><td>Name</td><td>Email</td><td>Message</td></tr>\n";
while ($line = pg_fetch_array($result, null, PGSQL_ASSOC)) {
$id=$line['id'];
$name=$line['name'];
$email=$line['email'];
$message=$line['message'];
echo "\t<tr><td>$name</td><td>$email</td><td>$message</td></tr>\n";
}
echo "</table>\n";
// Libère le résultat
pg_free_result($result);
// Ferme la connexion
pg_close($dbconn);
?>
Envoyer un message :
<form action=do_message.php method=post>
<input type=text name=message>
<input type=submit>
</form>

9
tp5-files/webapp/db.inc.php Executable file
View File

@ -0,0 +1,9 @@
<?php
$logindb = 'www';
$passworddb = 'foo';
$dbconn = pg_connect("host=postgres dbname=clientsdb user=$logindb password=$passworddb")
or die('Connexion impossible : ' . pg_last_error());
?>

View File

@ -0,0 +1,24 @@
<?php
session_start();
ob_start();
echo "Login is ". $_SESSION['login'];
echo "&nbsp;<a href=login.php?logout=1>Logout</a><br/><br/>";
require_once("../db.inc.php");
// Gestion des modifications
if (isset($_POST['name'])) { // Modification d'un client existant
$name=$_POST['name'];
$email=$_POST['email'];
$comment=$_POST['comment'];
$id=$_POST['id'];
$message=$_POST['message'];
$query = "UPDATE clients SET name='$name',email='$email',comment='$comment',message='$message' WHERE id='$id'";
pg_query($dbconn, $query) or die('Échec de la requête : ' . pg_last_error());
}
// Ferme la connexion
pg_close($dbconn);
header('Location: print_clients.php');
?>

View File

@ -0,0 +1,18 @@
<?php
session_start();
$_SESSION['login']=$_POST['login'];
$_SESSION['password']=$_POST['password'];
require_once("../db.inc.php");
$login = $_POST['login'];
$password = $_POST['password'];
$query = "SELECT * FROM clients WHERE name='$login' AND password='$password' AND role='helpdesk'";
$result = pg_query($dbconn, $query) or die('Échec de la requête : ' . pg_last_error());
if (pg_num_rows($result) > 0)
header('Location: print_clients.php');
else
print "Authentication failed, $query";
?>

View File

@ -0,0 +1,17 @@
<?
session_start();
unset($_SESSION['login']);
unset($_SESSION['password']);
?>
<html><head><title>Login Helpdesk</title></head>
<body>
<form action="do_login.php" method="post">
Login: <input type="text" name="login"><br/>
Password: <input type="password" name="password"><br/>
<input type="submit" value="Submit">
</form>
</body>
</html>

View File

@ -0,0 +1,33 @@
<?php
session_start();
echo "Login is ". $_SESSION['login'];
echo "&nbsp;<a href=index.php?logout=1>Logout</a><br/><br/>";
require_once("../db.inc.php");
// Affichage de la table des clients
// Exécution de la requête SQL
$query = 'SELECT * FROM clients';
$result = pg_query($dbconn, $query) or die('Échec de la requête : ' . pg_last_error());
// Affichage des résultats en HTML
echo "<table border=1><tr><td>Name</td><td>Email</td><td>Comment</td><td>Message</td><td>Name</td><td>Email</td><td>Comment</td><td>Message</td><td>Update</td></tr>\n";
while ($line = pg_fetch_array($result, null, PGSQL_ASSOC)) {
$id=$line['id'];
$name=$line['name'];
$email=$line['email'];
$comment=$line['comment'];
$message=$line['message'];
echo "\t<tr><td>$name</td><td>$email</td><td>$comment</td><td>&nbsp;$message</td>\n";
echo "\t<form action=do_helpdesk.php method=post><td><input type=text name=name value=\"$name\"></td><td><input type=text name=email value=$email></td><td><input type=text name=comment value=\"$comment\"></td><td><input type=text name=message value=\"$message\"></td><td><input type=submit value=Update><input type=hidden name=id value=$id></td></form></tr>\n";
}
echo "</table>\n";
// Libère le résultat
pg_free_result($result);
// Ferme la connexion
pg_close($dbconn);
?>