Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1219 minIntermédiaire

Sécuriser une application C# : secrets, mots de passe et données

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • repérer ce qu'un utilisateur peut lire ou modifier sur son poste ;
  • sortir la chaîne de connexion du code et du dépôt Git ;
  • créer un compte MySQL qui n'a que les droits utiles ;
  • hacher et vérifier un mot de passe avec BCrypt ou PBKDF2 ;
  • protéger les données de santé d'une application métier.

4 exercices corrigés

Sécuriser un client lourd, c'est partir d'un constat : l'application tourne sur un poste que l'utilisateur contrôle. Il peut lire les fichiers de configuration, décompiler l'exécutable, observer le trafic réseau. Ce chapitre applique ce constat à GSB Ordonnances : où ranger la chaîne de connexion, quels droits donner au compte MySQL, comment stocker les mots de passe des utilisateurs, et comment protéger des données de santé. Les principes généraux de la sécurité applicative sont dans le cours Sécurité applicative.

Ce que l'utilisateur d'un poste peut voir

Une application .NET est compilée en code intermédiaire, qui contient encore les noms des classes, des méthodes et les chaînes de caractères. Des outils libres de décompilation reconstruisent un code C# très proche de l'original en quelques secondes. Conséquences :

  • une chaîne de connexion écrite dans le code est lisible par quiconque copie l'exécutable ;
  • un fichier appsettings.json posé à côté de l'exécutable est lisible avec le Bloc-notes ;
  • une vérification faite seulement dans l'interface (un bouton caché pour les non-administrateurs) se contourne.

Sortir la chaîne de connexion du code

La chaîne de connexion change selon l'environnement (ton poste, le serveur de test, la production) et contient un mot de passe. Elle n'a rien à faire dans le code source, ni dans le dépôt Git : un mot de passe poussé sur GitHub reste dans l'historique même après correction.

On lit la configuration avec le paquet Microsoft.Extensions.Configuration, qui superpose plusieurs sources, la dernière l'emportant :

bash
dotnet add Gsb.Ui package Microsoft.Extensions.Configuration.Json
dotnet add Gsb.Ui package Microsoft.Extensions.Configuration.EnvironmentVariables
dotnet add Gsb.Ui package Microsoft.Extensions.Configuration.UserSecrets
csharp
using Microsoft.Extensions.Configuration;

IConfiguration config = new ConfigurationBuilder()
    .SetBasePath(AppContext.BaseDirectory)
    .AddJsonFile("appsettings.json", optional: true)            // 1. le fichier livré avec l'application
    .AddUserSecrets(typeof(Program).Assembly, optional: true)   // 2. les secrets du développeur
    .AddEnvironmentVariables(prefix: "GSB_")                    // 3. les variables d'environnement
    .Build();

string? chaine = config.GetConnectionString("Gsb");
SourceOù se trouve-t-elle ?Usage
appsettings.jsonÀ côté de l'exécutableLes réglages non secrets : adresse du serveur, nom de la base, SslMode. Un modèle appsettings.exemple.json est versionné, le vrai fichier ne l'est pas
Secrets utilisateurDans le profil Windows du développeur (%APPDATA%\Microsoft\UserSecrets\…), hors du dossier du projetTon mot de passe de développement : il ne peut pas partir dans Git par erreur
Variables d'environnementLe système ou la sessionLes postes de test, l'intégration continue (secrets dans un pipeline)

Les secrets utilisateur s'initialisent et se renseignent en ligne de commande, dans le dossier du projet :

bash
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Gsb" "Server=localhost;Database=gsb_ordonnances;User ID=gsb_dev;Password=…"

Avec la variable d'environnement, le séparateur : devient un double tiret bas : GSB_ConnectionStrings__Gsb. Le fichier appsettings.json ressemble à ceci, et doit être copié dans le dossier de sortie (propriété « Copier dans le répertoire de sortie » à « Copier si plus récent ») :

json
{
  "ConnectionStrings": {
    "Gsb": "Server=srv-bdd.cabinet.local;Database=gsb_ordonnances;User ID=gsb_app;SslMode=Required"
  }
}

Les secrets utilisateur ne servent qu'en développement. Sur les postes du cabinet, le mot de passe peut être chiffré pour l'utilisateur Windows avec l'API DPAPI de Windows : seul ce compte Windows, sur ce poste, peut le déchiffrer.

csharp
using System.Security.Cryptography;   // paquet System.Security.Cryptography.ProtectedData
using System.Text;

byte[] chiffre = ProtectedData.Protect(Encoding.UTF8.GetBytes(chaine), null, DataProtectionScope.CurrentUser);
File.WriteAllBytes(fichier, chiffre);

// au démarrage suivant
string relue = Encoding.UTF8.GetString(
    ProtectedData.Unprotect(File.ReadAllBytes(fichier), null, DataProtectionScope.CurrentUser));

DPAPI protège contre la copie du fichier sur une autre machine ou sa lecture par un autre compte, pas contre l'utilisateur lui-même : il reste une protection de second rang.

Un compte MySQL qui n'a que les droits utiles

L'application ne se connecte jamais avec root. On lui crée un compte qui n'a que les droits dont elle a besoin, sur les seules tables qu'elle utilise, et depuis le seul réseau du cabinet : c'est le moindre privilège (GRANT et REVOKE).

sql
CREATE USER 'gsb_app'@'192.168.10.%' IDENTIFIED BY 'un-mot-de-passe-long-et-unique';

GRANT SELECT, INSERT, UPDATE, DELETE ON gsb_ordonnances.patient     TO 'gsb_app'@'192.168.10.%';
GRANT SELECT, INSERT                 ON gsb_ordonnances.ordonnance  TO 'gsb_app'@'192.168.10.%';
GRANT SELECT, INSERT                 ON gsb_ordonnances.contenir    TO 'gsb_app'@'192.168.10.%';
GRANT SELECT                         ON gsb_ordonnances.medecin     TO 'gsb_app'@'192.168.10.%';
GRANT SELECT                         ON gsb_ordonnances.medicament  TO 'gsb_app'@'192.168.10.%';
GRANT SELECT                         ON gsb_ordonnances.utilisateur TO 'gsb_app'@'192.168.10.%';

Ce compte ne peut ni supprimer une table, ni créer un compte, ni modifier une ordonnance déjà émise, ni lire la base d'une autre application. Si son mot de passe fuit, les dégâts restent limités, et le pare-feu du serveur n'accepte de toute façon les connexions au port 3306 que depuis le réseau du cabinet. Ajoute SslMode=Required dans la chaîne de connexion pour que les données de santé ne circulent jamais en clair.

Stocker les mots de passe des utilisateurs : le hachage

L'application a ses propres utilisateurs (la secrétaire, les médecins), rangés dans la table utilisateur. Leur mot de passe ne doit jamais y être stocké en clair, ni même chiffré : on stocke une empreinte (hash) calculée par une fonction de hachage lente, avec un sel aléatoire. À la connexion, on recalcule l'empreinte du mot de passe saisi et on la compare. Si la table fuit, l'attaquant n'obtient pas les mots de passe, et la lenteur volontaire de la fonction rend très coûteux de les retrouver par essais successifs (stocker un mot de passe).

AlgorithmeÀ utiliser ?Pourquoi
Stockage en clair, Base64JamaisLisible directement en cas de fuite
MD5, SHA-1, SHA-256 seulsJamais pour un mot de passeTrop rapides : des milliards d'essais par seconde sur une carte graphique
bcryptOuiLent, salé, paramétrable (facteur de coût)
PBKDF2 avec SHA-256OuiInclus dans .NET, sans paquet ; l'OWASP recommande au moins 600 000 itérations
Argon2idOui, le plus récentRésiste aussi aux attaques par matériel spécialisé ; nécessite une bibliothèque tierce en .NET

Avec BCrypt.Net-Next. Le paquet BCrypt.Net-Next génère le sel, le range dans l'empreinte et vérifie en une ligne :

csharp
string empreinte = BCrypt.Net.BCrypt.HashPassword("Ordonnance-2026!", workFactor: 12);
// $2a$12$KaivI.GwRD8Nh62xfaOgIuRJf5gEFyf74DbwX3HvtRMiJXNE1UTvS  (60 caractères)

bool correct = BCrypt.Net.BCrypt.Verify("Ordonnance-2026!", empreinte);   // true
bool faux = BCrypt.Net.BCrypt.Verify("ordonnance-2026!", empreinte);      // false

L'empreinte contient tout ce qu'il faut pour vérifier : $2a$ (la variante de bcrypt), 12$ (le facteur de coût : 2¹² tours) et, sur les 22 caractères suivants, le sel. Hacher deux fois le même mot de passe donne deux empreintes différentes, puisque le sel change. Avec un facteur de 12, un calcul prend de l'ordre d'un quart de seconde sur un PC récent : imperceptible pour l'utilisateur, très coûteux pour un attaquant qui essaie des millions de mots de passe. Note aussi que bcrypt ne prend en compte que les 72 premiers octets du mot de passe.

Avec PBKDF2, sans paquet. .NET fournit Rfc2898DeriveBytes.Pbkdf2. On génère soi-même le sel et on range dans la base l'algorithme, le nombre d'itérations, le sel et l'empreinte :

csharp
using System.Security.Cryptography;

public static class MotDePassePbkdf2
{
    private const int Iterations = 600_000;
    private static readonly HashAlgorithmName Algo = HashAlgorithmName.SHA256;

    public static string Hacher(string motDePasse)
    {
        byte[] sel = RandomNumberGenerator.GetBytes(16);
        byte[] cle = Rfc2898DeriveBytes.Pbkdf2(motDePasse, sel, Iterations, Algo, 32);
        return $"pbkdf2-sha256${Iterations}${Convert.ToBase64String(sel)}${Convert.ToBase64String(cle)}";
    }

    public static bool Verifier(string motDePasse, string stocke)
    {
        string[] parties = stocke.Split('$');
        if (parties.Length != 4 || parties[0] != "pbkdf2-sha256") return false;
        int iterations = int.Parse(parties[1]);
        byte[] sel = Convert.FromBase64String(parties[2]);
        byte[] attendu = Convert.FromBase64String(parties[3]);
        byte[] calcule = Rfc2898DeriveBytes.Pbkdf2(motDePasse, sel, iterations, Algo, attendu.Length);
        return CryptographicOperations.FixedTimeEquals(calcule, attendu);
    }
}

CryptographicOperations.FixedTimeEquals compare en temps constant : une comparaison ordinaire s'arrête au premier octet différent, et la durée de la réponse renseignerait un attaquant sur le nombre d'octets corrects.

La fenêtre de connexion

Le service d'authentification de la couche métier vérifie l'identifiant et le mot de passe. Il calcule une vérification bcrypt même quand l'identifiant n'existe pas, pour que la réponse prenne le même temps dans les deux cas : sinon, un attaquant saurait quels identifiants existent en mesurant le temps de réponse.

csharp
public class AuthService(IUtilisateurRepository depot)
{
    private static readonly string EmpreinteLeurre = BCrypt.Net.BCrypt.HashPassword("leurre", 12);

    public static string Hacher(string motDePasse) =>
        BCrypt.Net.BCrypt.HashPassword(motDePasse, workFactor: 12);

    public async Task<Utilisateur?> ConnecterAsync(string login, string motDePasse)
    {
        Utilisateur? utilisateur = await depot.TrouverParLoginAsync(login);
        string empreinte = utilisateur?.HashMotDePasse ?? EmpreinteLeurre;
        bool correct = BCrypt.Net.BCrypt.Verify(motDePasse, empreinte);
        return correct && utilisateur is not null ? utilisateur : null;
    }
}

Le formulaire de connexion ajoute trois protections : un message unique (« Identifiant ou mot de passe incorrect », jamais « identifiant inconnu »), une attente croissante après chaque échec, et une limite d'essais.

csharp
private async void btnConnexion_Click(object? sender, EventArgs e)
{
    btnConnexion.Enabled = false;
    lblErreur.Text = "";
    try
    {
        Utilisateur = await _auth.ConnecterAsync(txtLogin.Text.Trim(), txtMotDePasse.Text);
        if (Utilisateur is not null)
        {
            DialogResult = DialogResult.OK;
            return;
        }

        _echecs++;
        txtMotDePasse.Clear();
        if (_echecs >= EssaisMax)
        {
            MessageBox.Show("Trop d'essais. L'application va se fermer.", "Connexion",
                MessageBoxButtons.OK, MessageBoxIcon.Stop);
            DialogResult = DialogResult.Cancel;
            return;
        }
        lblErreur.Text = "Identifiant ou mot de passe incorrect.";
        await Task.Delay(TimeSpan.FromSeconds(_echecs));   // 1 s, puis 2 s, puis 3 s…
    }
    catch (Exception)
    {
        lblErreur.Text = "Serveur injoignable. Réessaie dans un instant.";
    }
    finally
    {
        btnConnexion.Enabled = true;
    }
}

Un compteur dans le formulaire se contourne en relançant l'application. Pour un vrai verrouillage, le nombre d'échecs et la date du dernier échec sont enregistrés dans la table utilisateur, côté serveur. Les règles de mots de passe (longueur, renouvellement, double authentification) sont détaillées dans le cours Authentification.

Les rôles : cacher ne suffit pas

La secrétaire gère les patients ; seul un médecin crée une ordonnance. Dans l'interface, on masque les boutons inutiles (btnNouvelleOrdonnance.Visible = utilisateur.Role == "medecin";). Mais masquer est du confort : la vraie vérification se fait dans la couche métier (OrdonnanceService refuse une ordonnance créée par un utilisateur qui n'a pas le rôle médecin, voir le chapitre 8 ; le chapitre 13 la teste) et, idéalement, dans la base ou l'API. Le contrôle d'accès côté serveur est le sujet du chapitre contrôle d'accès défaillant.

Des données de santé

Les informations sur la santé d'un patient (ses ordonnances, ses traitements) sont des données de santé, une catégorie particulière du RGPD (article 9) dont le traitement est en principe interdit, sauf exceptions, comme la prise en charge médicale par des professionnels soumis au secret. Pour l'application, cela impose (RGPD : principes et obligations) :

  • la minimisation : ne collecter et n'afficher que ce qui est utile (l'export CSV du chapitre 4 ne contient pas le numéro de sécurité sociale) ;
  • la confidentialité : connexion chiffrée (SslMode=Required), comptes nominatifs, droits par rôle, poste verrouillé en cas d'absence ;
  • la traçabilité : journaliser qui a consulté ou modifié quoi, sans écrire de données de santé dans les journaux ;
  • un hébergement adapté : en France, un prestataire qui héberge des données de santé pour le compte d'un cabinet doit être certifié HDS (hébergeur de données de santé).

La propriété Form.FormScreenCaptureMode, apparue avec .NET 10, peut empêcher les logiciels de capture d'écran de photographier une fenêtre sensible (FormScreenCaptureMode = ScreenCaptureMode.HideContent;) : une protection d'appoint, pas une garantie.

Exercices

Exercice 1 sur 4 Trouve les failles Réussi À revoir

Relève toutes les failles de sécurité de ce code de connexion, et dis comment corriger chacune.

csharp
private void btnConnexion_Click(object? sender, EventArgs e)
{
    var conn = new MySqlConnection("Server=192.168.10.5;Database=gsb_ordonnances;User ID=root;Password=Gsb2026");
    conn.Open();
    var cmd = new MySqlCommand("SELECT role FROM utilisateur WHERE login = '" + txtLogin.Text
        + "' AND motDePasse = '" + txtMdp.Text + "'", conn);
    object? role = cmd.ExecuteScalar();
    if (role == null) MessageBox.Show("Identifiant inconnu ou mot de passe faux");
    else new FrmPatients(role.ToString()!).Show();
}
Voir la réponseMasquer la réponse de l'exercice 1
  1. Chaîne de connexion dans le code : lisible par décompilation et poussée dans Git. La lire depuis la configuration (fichier non versionné, secrets utilisateur, variable d'environnement, DPAPI).
  2. Compte root : un attaquant qui récupère la chaîne contrôle tout le serveur. Créer un compte applicatif aux droits minimaux, limité au réseau du cabinet.
  3. Mot de passe faible et partagé (Gsb2026) : utiliser un mot de passe long et unique.
  4. Injection SQL : la saisie est concaténée dans la requête. Utiliser une requête paramétrée.
  5. Mot de passe stocké en clair dans la colonne motDePasse et comparé en SQL : stocker une empreinte bcrypt ou PBKDF2 et la vérifier en C# avec BCrypt.Verify.
  6. Connexion non chiffrée (pas de SslMode=Required) : les mots de passe et les données de santé circulent en clair sur le réseau.
  7. Aucune limite d'essais ni attente : on peut essayer des milliers de mots de passe. Ajouter une attente croissante et un verrouillage côté serveur.
  8. Connexion jamais fermée (pas de using) : sans gravité pour la sécurité, mais à corriger.

Le message unique « identifiant inconnu ou mot de passe faux » est, lui, correct : il ne dit pas lequel des deux est faux.

Tu avais trouvé ?

Exercice 2 sur 4 Les droits du compte applicatif Réussi À revoir

L'application doit maintenant permettre à la secrétaire de corriger la posologie d'une ligne d'ordonnance, mais jamais de supprimer une ordonnance. Quelle instruction ajoutes-tu au compte gsb_app ? Faut-il lui donner DELETE sur contenir ?

Voir la réponseMasquer la réponse de l'exercice 2
sql
GRANT UPDATE (posologie) ON gsb_ordonnances.contenir TO 'gsb_app'@'192.168.10.%';

Le droit est accordé sur la seule colonne posologie : le compte ne peut modifier ni le médicament, ni la durée, ni le numéro d'ordonnance. Non, il ne faut pas donner DELETE sur contenir : ce n'est pas demandé, et ce droit permettrait de vider une ordonnance de ses lignes. La règle « la secrétaire et pas le médecin » se vérifie, elle, dans la couche métier, puisque les deux utilisent le même compte MySQL.

Tu avais trouvé ?

Exercice 3 sur 4 Vérifier un mot de passe Réussi À revoir

La table utilisateur contient, pour secretariat, l'empreinte $2a$12$KaivI.GwRD8Nh62xfaOgIuRJf5gEFyf74DbwX3HvtRMiJXNE1UTvS.

  1. Quel est le facteur de coût ?
  2. Un collègue propose de calculer BCrypt.HashPassword(saisie) et de comparer le résultat à la colonne avec ==. Pourquoi cela ne marche-t-il jamais ?
  3. Que faut-il écrire à la place ?
Voir la réponseMasquer la réponse de l'exercice 3
  1. 12 (le nombre après $2a$) : 2¹² = 4 096 tours.
  2. HashPassword tire un nouveau sel à chaque appel : l'empreinte obtenue est différente de celle stockée, même pour le bon mot de passe. La comparaison avec == est toujours fausse.
  3. BCrypt.Net.BCrypt.Verify(saisie, empreinteStockee), qui relit le sel et le coût dans l'empreinte stockée, recalcule et compare.

Tu avais trouvé ?

Exercice 4 sur 4 Où ranger le secret ? Réussi À revoir

Pour chaque situation, choisis où se trouve la chaîne de connexion.

  1. Ton poste de développement personnel.
  2. Le serveur d'intégration continue qui lance les tests d'intégration.
  3. Les postes Windows du cabinet, en production.
  4. Le dépôt GitHub du projet.
Voir la réponseMasquer la réponse de l'exercice 4
  1. Dans les secrets utilisateur (dotnet user-secrets set …), hors du dossier du projet.
  2. Dans une variable secrète du pipeline, exposée comme variable d'environnement GSB_ConnectionStrings__Gsb.
  3. Dans appsettings.json pour les réglages non secrets, et le mot de passe chiffré par DPAPI pour le compte Windows de l'utilisateur ; mieux encore, pas de mot de passe de base du tout si l'application passe par une API.
  4. Nulle part : seulement un appsettings.exemple.json sans mot de passe, et le vrai fichier dans le .gitignore.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Tests unitaires en C# avec MSTest (ou xUnit)Revoir le sommaire du cours