CI/CD & Ops

Qualité et sécurité

SonarQube, analyzers, couverture, OWASP Top 10.

Vérifié en septembre 2026 · outils aux versions citées dans le cours · environ 18 min

Un pipeline qui compile et fait passer les tests prouve que le code fait ce que ses tests vérifient. Il ne dit rien d'une exception relancée qui perd sa pile, d'un signal affiché sans être appelé, d'une requête SQL assemblée à partir d'une saisie, ni d'une dépendance dont la faille a été publiée hier. Les contrôles de qualité et de sécurité ferment ces trous, et ils ne valent que s'ils bloquent : un avertissement que personne ne lit n'arrête rien. Ce cours les prend dans l'ordre où un pipeline les rencontre : les analyseurs du compilateur, .NET puis Angular, la couverture des tests, la porte qualité de SonarQube, puis la sécurité, de l'OWASP Top 10 aux dépendances vulnérables. Les sorties sont celles du SDK .NET 10.0.300, en français, et d'Angular 22.1.7 ; la syntaxe des pipelines qui enchaînent ces étapes, jobs, artefacts, rapports et variables secrètes, est le sujet des cours « GitLab CI » et « Azure DevOps », qui ne montrent de ces contrôles que l'audit npm.

Les analyseurs .NET dans le build

Le SDK .NET embarque deux familles d'analyseurs Roslyn, qui tournent pendant la compilation : les règles de qualité, CA suivi d'un numéro, et les règles de style, IDE suivi d'un numéro. Sans réglage, un projet .NET 10 n'en signale qu'une poignée, comme CA2200 ; d'autres sont des suggestions de l'éditeur, beaucoup sont désactivées. Quatre propriétés MSBuild décident de ce que le build signale, et de ce qu'il refuse.

PropriétéRôleDéfaut
AnalysisLevel quel jeu de règles : latest, preview ou figé, 10.0latest
AnalysisMode combien de règles deviennent des avertissements : None, Default, Minimum, Recommended, AllDefault
EnforceCodeStyleInBuildexécute aussi les règles de style au buildfalse
TreatWarningsAsErrorsfait de chaque avertissement une erreurfalse
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

    <!-- Les regles de .NET 10, en mode Recommended ; forme composee : 10.0-recommended -->
    <AnalysisLevel>10.0</AnalysisLevel>
    <AnalysisMode>Recommended</AnalysisMode>
    <!-- Les regles de style IDExxxx s'executent aussi au build -->
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

    <!-- Exige par IDE0005 au build ; CS1591 : pas de commentaire XML impose -->
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <NoWarn>$(NoWarn);CS1591</NoWarn>
  </PropertyGroup>

</Project>
using System.Text;

namespace Boutique
{
    public class Facture
    {
        public decimal Total { get; set; }

        public string Libelle(int numero) => string.Format("Facture {0} : {1:C}", numero, Total);

        public bool EstVide(List<string> lignes) => lignes.Count() == 0;

        public void Valider()
        {
            try
            {
                if (Total < 0) throw new InvalidOperationException("Total negatif");
            }
            catch (InvalidOperationException ex)
            {
                throw ex;
            }
        }
    }
}
$ dotnet build
Restauration terminée (0,5s)
  Boutique net10.0 a échoué avec 5 erreur(s) (0,4s)
    C:\src\Boutique\Facture.cs(3,1): error IDE0161: Convertir en namespace inclus dans l'étendue de fichier (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0161)
    C:\src\Boutique\Facture.cs(9,46): error CA1305: Le comportement de 'string.Format(string, object, object)' peut varier en fonction des paramètres régionaux de l'utilisateur actuel. Remplacez cet appel dans 'Facture.Libelle(int)' par un appel à 'string.Format(IFormatProvider, string, params object[])'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
    C:\src\Boutique\Facture.cs(21,17): error CA2200: Quand une exception interceptée est levée à nouveau, cela entraîne un changement des informations de la pile (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200)
    C:\src\Boutique\Facture.cs(1,1): error IDE0005: La directive using n'est pas nécessaire. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)
    C:\src\Boutique\Facture.cs(11,53): error CA1829: Utilisez la propriété "Count" à la place de Enumerable.Count() (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1829)

Générer a échoué avec 5 erreur(s) dans 1,5s

Le projet est placé dans C:\src\Boutique, et l'ordre des lignes varie d'un build à l'autre. Le mode Recommended ajoute CA1305, un format qui dépend de la culture de la machine, et CA1829, un Count() de LINQ sur une liste qui a déjà sa propriété ; dans le SDK 10.0.300, son fichier de configuration passe 145 règles en avertissement, contre 280 pour All. CA2200, le throw ex; qui efface la pile d'origine que décrit le cours « Exceptions et gestion d'erreur », est actif même sans réglage. TreatWarningsAsErrors transforme le tout en erreurs, avertissements du compilateur compris.

AnalysisLevel se fige pour une raison simple : à latest, une mise à jour du SDK apporte ses nouvelles règles, et avec TreatWarningsAsErrors un build qui passait hier échoue sans qu'une ligne ait changé. Figé à 10.0, le jeu de règles ne bouge que quand l'équipe le décide ; la forme composée 10.0-recommended règle les deux propriétés d'un coup. Certaines équipes ne durcissent que le pipeline, par dotnet build -warnaserror, pour ne pas bloquer un poste au milieu d'une refonte.

Régler les sévérités par .editorconfig

Le mode fixe un point de départ ; le fichier .editorconfig, à la racine du dépôt, ajuste règle par règle, et l'éditeur comme le build lisent le même. Une sévérité vaut none, silent, suggestion, warning ou error ; seules les deux dernières apparaissent dans la sortie de dotnet build.

root = true

[*.cs]
# Style : en suggestion, l'IDE le propose ; en warning, le build le signale.
csharp_style_namespace_declarations = file_scoped
dotnet_diagnostic.IDE0161.severity = warning
dotnet_diagnostic.IDE0005.severity = warning

# Qualite : on garde l'avis de l'analyseur, sans bloquer.
dotnet_diagnostic.CA1822.severity = suggestion

# Securite : jamais de compromis.
dotnet_diagnostic.CA2100.severity = error

Les deux règles de style de la sortie y figurent parce qu'EnforceCodeStyleInBuild est actif et que ce fichier les monte en warning. IDE0005, la directive using inutile, cache une condition : elle ne tourne au build qu'avec GenerateDocumentationFile, faute de quoi le compilateur répond par le diagnostic EnableGenerateDocumentationFile ; le NoWarn de CS1591 évite alors un avertissement par membre public sans commentaire XML. CA1822 descend en suggestion : visible dans l'éditeur, sans bloquer. CA2100, une commande SQL dont le texte vient d'une chaîne construite, n'est active qu'en mode All : le fichier l'impose en erreur.

Angular : strict, strictTemplates et diagnostics étendus

Le front a ses contrôles, en trois couches. strict, côté TypeScript, vérifie le code des classes ; c'est le défaut depuis TypeScript 6.0, et le cours « Configuration et écosystème » détaille ce qu'il refuse. strictTemplates, côté Angular, vérifie les templates avec la même rigueur : type des entrées et des sorties, null dans les liaisons, variables de @for et de @if. C'est le défaut depuis Angular 22.0, sorti le 3 juin 2026 ; sur un projet existant, la migration de ng update écrit "strictTemplates": false pour garder l'ancien comportement, et c'est la première ligne à retirer une fois la mise à jour faite. Les diagnostics étendus, enfin, signalent du code valide mais presque sûrement faux ; ils exigent strictTemplates et sortent par défaut en avertissements. Avec ces options dans le tsconfig.json, un composant fautif donne la sortie qui suit.

{
  "compilerOptions": {
    "strict": true
  },
  "angularCompilerOptions": {
    "strictTemplates": true,
    "extendedDiagnostics": {
      "defaultCategory": "error",
      "checks": {
        "nullishCoalescingNotNullable": "warning"
      }
    }
  }
}
import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';

@Component({
  selector: 'app-ligne',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<span>{{ quantite() }}</span>`,
})
export class Ligne {
  readonly quantite = input.required<number>();
}

@Component({
  selector: 'app-panier',
  imports: [Ligne],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>{{ total }} euros</p>
    <p>{{ client.nom ?? 'anonyme' }}</p>
    <app-ligne [quantite]="'3'" />
    <button (click)="valider">Valider</button>
  `,
})
export class Panier {
  protected readonly total = signal(42);
  protected readonly client = { nom: 'Ada' };
  protected valider(): void {}
}
$ ng build
Application bundle generation failed. [5.808 seconds] - 2026-09-26T09:02:06.990Z

▲ [WARNING] NG8102: NG8102: The left side of this nullish coalescing operation does not include 'null' or 'undefined' in its type, therefore the '??' operator can be safely removed. Find more at https://v22.angular.dev/extended-diagnostics/NG8102 [plugin angular-compiler]

    src/app/panier.ts:18:10:
      18 │     <p>{{ client.nom ?? 'anonyme' }}</p>
         ╵           ~~~~~~~~~~~~~~~~~~~~~~~


X [ERROR] NG8109: NG8109: total is a function and should be invoked: total()}. Find more at https://v22.angular.dev/extended-diagnostics/NG8109 [plugin angular-compiler]

    src/app/panier.ts:17:10:
      17 │     <p>{{ total }} euros</p>
         ╵           ~~~~~


X [ERROR] NG8117: NG8117: Function in text interpolation should be invoked: total(). Find more at https://v22.angular.dev/extended-diagnostics/NG8117 [plugin angular-compiler]

    src/app/panier.ts:17:10:
      17 │     <p>{{ total }} euros</p>
         ╵           ~~~~~


X [ERROR] TS2322: Type 'string' is not assignable to type 'number'. [plugin angular-compiler]

    src/app/panier.ts:19:16:
      19 │     <app-ligne [quantite]="'3'" />
         ╵                 ~~~~~~~~


X [ERROR] NG8111: NG8111: Function in event binding should be invoked: valider(). Find more at https://v22.angular.dev/extended-diagnostics/NG8111 [plugin angular-compiler]

    src/app/panier.ts:20:12:
      20 │     <button (click)="valider">Valider</button>
         ╵             ~~~~~~~~~~~~~~~~~

TS2322 vient de strictTemplates : sans lui, l'entrée recevrait la chaîne '3' sans un mot. NG8109 et NG8117 disent la même faute de deux façons, un signal affiché comme une fonction, et NG8111 un clic qui n'appelle rien. defaultCategory les passe en erreurs, et checks règle chaque diagnostic : NG8102, le ?? inutile, reste un avertissement.

Un piège tient au texte du fichier, pas à la valeur de l'option. Angular 22.1.7 n'active NG8102 et NG8107, les ?? et ?. inutiles, que si strict ou strictNullChecks est écrit dans le tsconfig : la valeur par défaut de TypeScript 6.0, pourtant stricte, ne compte pas. Or le tsconfig.json que produit ng new en 22.1 n'écrit ni strict ni strictTemplates : sur un projet neuf, ces deux diagnostics se taisent. Ce site écrit "strict": true pour cette raison, avec "strictTemplates": true ; il ne lui manque que extendedDiagnostics. Le cours « Configuration et écosystème » part de ce même fichier.

La couverture de code

La couverture mesure quelles lignes et quelles branches les tests ont exécutées. En .NET, deux collecteurs se branchent sur dotnet test : coverlet, que le modèle dotnet new xunit décrit dans le cours « xUnit » référence déjà, et Microsoft.CodeCoverage, qu'apporte Microsoft.NET.Test.Sdk. Les deux écrivent du Cobertura, le format XML que lisent GitLab, Azure DevOps et SonarQube.

namespace Boutique;

public static class Remise
{
    // 10 % des 100 euros, 5 % de plus pour un client fidele.
    public static decimal Appliquer(decimal montant, bool clientFidele)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(montant);
        var taux = montant >= 100m ? 0.10m : 0m;
        if (clientFidele) taux += 0.05m;
        return montant * (1 - taux);
    }
}
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <!-- Le modele dotnet new xunit du SDK 10.0.300 ecrit 6.0.4, 17.14.1 et 3.1.4 -->
    <PackageReference Include="coverlet.collector" Version="10.0.1" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" /> <!-- apporte Microsoft.CodeCoverage 18.8.1 -->
    <PackageReference Include="xunit" Version="2.9.3" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="Xunit" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Boutique.Domaine\Boutique.Domaine.csproj" />
  </ItemGroup>

</Project>

Une ligne couverte est une ligne exécutée, pas une ligne vérifiée. Le premier test atteint toutes les lignes et toutes les branches de Remise, et n'affirme rien :

namespace Boutique.Tests;

public class RemiseTests
{
    [Fact]
    public void Appliquer_fonctionne()
    {
        Remise.Appliquer(150m, clientFidele: true);
        Remise.Appliquer(50m, clientFidele: false);
        try
        {
            Remise.Appliquer(-1m, clientFidele: false);
        }
        catch (ArgumentOutOfRangeException)
        {
        }
    }
}
namespace Boutique.Tests;

public class RemiseTests
{
    [Theory]
    [InlineData(50, false, 50)]
    [InlineData(50, true, 47.5)]
    [InlineData(100, false, 90)]
    [InlineData(150, true, 127.5)]
    public void Appliquer_calcule_le_montant_remise(decimal montant, bool fidele, decimal attendu)
    {
        Assert.Equal(attendu, Remise.Appliquer(montant, fidele));
    }

    [Fact]
    public void Appliquer_refuse_un_montant_negatif()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() => Remise.Appliquer(-1m, clientFidele: false));
    }
}

Le second compare chaque résultat à une valeur calculée à la main et vérifie le refus d'un montant négatif. Les deux donnent pourtant la même mesure, 6 lignes sur 6 et 4 branches sur 4. La différence n'apparaît qu'en faussant le code : le taux fidèle passe de 0.05m à 0.50m.

# Depuis le dossier de Boutique.slnx : sans solution ni projet dans le dossier
# courant, dotnet test s'arrete sur MSB1003.
# 1. Le premier test, sur le code juste
$ dotnet test --collect:"XPlat Code Coverage"
Restauration terminée (3,7s)
  Boutique.Domaine net10.0 a réussi (0,5s) → Boutique.Domaine\bin\Debug\net10.0\Boutique.Domaine.dll
  Boutique.Tests net10.0 a réussi (0,9s) → Boutique.Tests\bin\Debug\net10.0\Boutique.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.5+1b188a7b0a (64-bit .NET 10.0.8)
[xUnit.net 00:00:00.14]   Discovering: Boutique.Tests
[xUnit.net 00:00:00.19]   Discovered:  Boutique.Tests
[xUnit.net 00:00:00.23]   Starting:    Boutique.Tests
[xUnit.net 00:00:00.31]   Finished:    Boutique.Tests
  Test de Boutique.Tests net10.0 : a réussi (3,1 s)

Récapitulatif du test : total : 1; échec : 0; réussi : 1; ignoré : 0; durée : 3,1s
Générer a réussi dans 8,6s

Pièces jointes :
  C:\src\Couverture\Boutique.Tests\TestResults\548ca8a6-81a6-47e1-8d65-7d9ffbedc8ca\coverage.cobertura.xml

$ head -2 Boutique.Tests/TestResults/548ca8a6-81a6-47e1-8d65-7d9ffbedc8ca/coverage.cobertura.xml
<?xml version="1.0" encoding="utf-8"?>
<coverage line-rate="1" branch-rate="1" version="1.9" timestamp="1790413042" lines-covered="6" lines-valid="6" branches-covered="4" branches-valid="4">

# 2. Remise.cs faussee (taux += 0.50m), premier test : extrait
Récapitulatif du test : total : 1; échec : 0; réussi : 1; ignoré : 0; durée : 3,0s
<coverage line-rate="1" branch-rate="1" version="1.9" timestamp="1790413051" lines-covered="6" lines-valid="6" branches-covered="4" branches-valid="4">

# 3. Meme erreur, second test : extrait, piles d'appels retirees
[xUnit.net 00:00:00.34]     Boutique.Tests.RemiseTests.Appliquer_calcule_le_montant_remise(montant: 150, fidele: True, attendu: 127,5) [FAIL]
[xUnit.net 00:00:00.34]       Assert.Equal() Failure: Values differ
[xUnit.net 00:00:00.34]       Expected: 127,5
[xUnit.net 00:00:00.34]       Actual:   60,00
[xUnit.net 00:00:00.35]     Boutique.Tests.RemiseTests.Appliquer_calcule_le_montant_remise(montant: 50, fidele: True, attendu: 47,5) [FAIL]
[xUnit.net 00:00:00.35]       Assert.Equal() Failure: Values differ
[xUnit.net 00:00:00.35]       Expected: 47,5
[xUnit.net 00:00:00.35]       Actual:   25,00
Récapitulatif du test : total : 5; échec : 2; réussi : 3; ignoré : 0; durée : 2,6s
<coverage line-rate="1" branch-rate="1" version="1.9" timestamp="1790413062" lines-covered="6" lines-valid="6" branches-covered="4" branches-valid="4">

Le premier test passe encore, couverture intacte ; le second échoue sur deux cas. La couverture dit donc où il n'y a sûrement pas de test, jamais où il y en a un bon, et en faire un objectif fabrique exactement le premier test ; le cours « TDD » la tient lui aussi pour un sous-produit. Fausser le code pour compter les tests qui le remarquent, comme ici à la main, est le principe du test de mutation, qui mesure la force des assertions.

Le collecteur de coverlet s'appelle « XPlat Code Coverage » ; Format=opencover en change le format, mais Cobertura, son défaut, suffit à SonarQube. Celui de Microsoft se demande par --collect "Code Coverage;Format=cobertura" ; il compte aussi l'assembly de test, 13 lignes au lieu de 6 ici, là où coverlet l'exclut. Côté Angular, ng test --coverage passe par Vitest, mais exige un fournisseur, @vitest/coverage-v8 ou @vitest/coverage-istanbul, que ce site n'installe pas : la commande s'y arrête sur « Code coverage requires either… ». Le builder se règle dans la cible test d'angular.json, ici d'après son schéma, sans exécution ; sous le seuil, la commande échoue.

{
  "builder": "@angular/build:unit-test",
  "options": {
    "coverage": true,
    "coverageReporters": ["text-summary", "cobertura"],
    "coverageThresholds": { "lines": 80, "branches": 80 }
  }
}

SonarQube : porte qualité et nouveau code

SonarQube rassemble ces mesures sur un serveur : il applique ses propres règles, importe la couverture, repère les duplications et garde l'historique des analyses. Au 26 septembre 2026, la dernière version est SonarQube Server 2026.4, la version à support long la 2026.1, et le scanner .NET, dotnet-sonarscanner, la 11.3.0 du 2 septembre 2026. Aucun serveur ne tourne ici : les commandes suivantes s'appuient sur leur documentation.

L'analyse .NET se fait en trois temps, parce qu'elle s'accroche au compilateur. begin télécharge le profil qualité du projet et prépare les analyseurs que le build chargera ; le build produit les constats, les tests la couverture ; end rassemble le tout et l'envoie.

# Agent de build ; SONAR_TOKEN est une variable secrete du pipeline.
dotnet tool install --global dotnet-sonarscanner --version 11.3.0

dotnet sonarscanner begin /k:"boutique-api" \
  /d:sonar.host.url="https://sonar.exemple.interne" \
  /d:sonar.token="$SONAR_TOKEN" \
  /d:sonar.cs.cobertura.reportsPaths="**/TestResults/*/coverage.cobertura.xml" \
  /d:sonar.qualitygate.wait=true

dotnet build Boutique.slnx --no-incremental --disable-build-servers
dotnet test Boutique.slnx --no-build --collect:"XPlat Code Coverage"

dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN"

--no-incremental force la recompilation : un projet que MSBuild juge à jour ne repasse pas par le compilateur, et ses constats manqueraient. --disable-build-servers évite qu'un processus MSBuild resté en vie verrouille les fichiers du scanner sur un agent Windows. Le jeton se passe à begin et à end, toujours par une variable secrète.

Le serveur juge ensuite l'analyse par une porte qualité (quality gate), une liste de conditions. Celle qu'il fournit, Sonar way, n'en pose que sur le nouveau code : aucun nouveau problème, tous les nouveaux points sensibles de sécurité revus, au moins 80 % de couverture et au plus 3 % de lignes dupliquées, ces deux dernières conditions ne comptant qu'à partir de 20 nouvelles lignes. Un projet ancien à 40 % de couverture passe donc la porte tant que ce qu'on y ajoute est propre, et la dette recule à mesure qu'on retouche l'ancien code : c'est la démarche que Sonar a longtemps appelée « Clean as You Code ». Le nouveau code se définit par projet : depuis la version précédente, le défaut, depuis un nombre de jours, ou par rapport à une branche de référence ; dans une merge request, c'est ce qui diffère de la branche cible, une analyse que la documentation réserve aux éditions Developer, Enterprise et Data Center. sonar.qualitygate.wait=true fait attendre le verdict au scanner, et échouer le pipeline si la porte reste fermée.

OWASP Top 10

L'OWASP Top 10 classe les risques de sécurité des applications web d'après des données de tests et un sondage de la profession. L'édition courante, la huitième, est celle de 2025, dévoilée en version candidate le 6 novembre 2025 à la conférence Global AppSec : elle fait entrer les défaillances de la chaîne d'approvisionnement logicielle et la mauvaise gestion des conditions exceptionnelles, et range la falsification de requêtes côté serveur (SSRF) dans le contrôle d'accès. Ce n'est pas une norme à cocher mais une carte des familles de failles ; le tableau donne une défense concrète pour chacune, et le cours qui la traite.

Catégorie 2025Défense en ASP.NET Core ou Angular
A01 Broken Access Control une politique par point de terminaison, et une règle sur la ressource chargée, jamais un identifiant d'URL cru sur parole — « Authentification et autorisation »
A02 Security Misconfiguration page d'exception du développeur hors production, HSTS — « Construire une API » ; CORS restreint — « Qualité d'API »
A03 Software Supply Chain Failuresaudit des dépendances, fichiers de verrouillage — plus bas
A04 Cryptographic Failures HTTPS partout, aucun algorithme maison, secrets hors du dépôt — « Configuration et journalisation »
A05 InjectionSQL paramétré, liaisons Angular qui échappent — ci-dessous
A06 Insecure Designmenaces pensées à la conception, limitation de débit — « Qualité d'API »
A07 Authentication Failures un fournisseur OpenID Connect plutôt que des mots de passe maison, validation complète du JWT — « Authentification et autorisation »
A08 Software or Data Integrity Failuressignatures des paquets vérifiées, désérialisation vers des types connus
A09 Security Logging and Alerting Failures refus 401 et 403 journalisés et suivis par des alertes ; pour les signaux eux-mêmes, « Observabilité »
A10 Mishandling of Exceptional Conditions un gestionnaire global qui répond en ProblemDetails, sans pile d'appels — « Qualité d'API »

Injection SQL

EF Core paramètre tout le SQL qu'il génère ; le risque revient dès qu'on en écrit soi-même. FromSqlRaw reçoit une chaîne déjà assemblée, et l'interpolation y colle la saisie telle quelle : l'apostrophe de l'attaquant ferme la chaîne SQL, et la condition devient toujours vraie.

using Microsoft.EntityFrameworkCore; // Microsoft.EntityFrameworkCore.Sqlite 10.0.12

using var db = new BoutiqueDb();
db.Database.OpenConnection(); // SQLite en memoire : la base vit tant que la connexion est ouverte
db.Database.EnsureCreated();
db.Clients.AddRange(new Client { Nom = "Ada" }, new Client { Nom = "Grace" });
db.SaveChanges();

var saisie = "x' OR '1'='1"; // ce qu'un attaquant tape dans le champ de recherche
Console.WriteLine($"{Recherche.Clients(db, saisie).Count} client(s)");

static class Recherche
{
    // warning EF1002: Method 'FromSqlRaw' inserts interpolated strings directly into the SQL,
    // without any protection against SQL injection. [...]
    public static List<Client> Clients(BoutiqueDb db, string nom) =>
        db.Clients.FromSqlRaw($"SELECT * FROM Clients WHERE Nom = '{nom}'").ToList();
}

sealed class Client
{
    public int Id { get; set; }
    public required string Nom { get; set; }
}

sealed class BoutiqueDb : DbContext
{
    public DbSet<Client> Clients => Set<Client>();

    protected override void OnConfiguring(DbContextOptionsBuilder options) =>
        options.UseSqlite("Data Source=:memory:");
}

// 2 client(s)
using Microsoft.EntityFrameworkCore;

// Client, BoutiqueDb et le programme principal : ceux de l'exemple precedent.
static class Recherche
{
    public static List<Client> Clients(BoutiqueDb db, string nom) =>
        db.Clients.FromSql($"SELECT * FROM Clients WHERE Nom = {nom}").ToList();
}

// 0 client(s)

FromSql reçoit un FormattableString : EF Core garde la saisie à part et l'envoie comme paramètre, et plus aucun client ne s'appelle x' OR '1'='1. L'analyseur EF1002, livré avec EF Core depuis la version 8, signale la première forme dès la compilation ; avec TreatWarningsAsErrors, elle ne passe plus.

XSS dans Angular

Angular tient toute valeur liée pour non fiable : l'interpolation l'échappe, et [innerHTML] la fait passer par son assainisseur, qui retire scripts et attributs d'événement. bypassSecurityTrustHtml lève cette protection pour une valeur ; appliqué à une saisie, il rouvre la faille :

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';

// Un avis de client, tel qu'il revient de l'API.
const AVIS = '<b>Super</b><img src="x" onerror="alert(document.cookie)">';

@Component({
  selector: 'app-avis',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<p [innerHTML]="avis"></p>`,
})
export class Avis {
  private readonly assainisseur = inject(DomSanitizer);
  protected readonly avis = this.assainisseur.bypassSecurityTrustHtml(AVIS);
}

// Rendu : <p><b>Super</b><img src="x" onerror="alert(document.cookie)"></p>
import { ChangeDetectionStrategy, Component } from '@angular/core';

const AVIS = '<b>Super</b><img src="x" onerror="alert(document.cookie)">';

@Component({
  selector: 'app-avis',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<p>{{ avis }}</p><p [innerHTML]="avis"></p>`,
})
export class Avis {
  protected readonly avis = AVIS;
}

// Rendu : <p>&lt;b&gt;Super&lt;/b&gt;&lt;img src="x" onerror="alert(document.cookie)"&gt;</p>
//         <p><b>Super</b><img src="x"></p>
// Console, en developpement : WARNING: sanitizing HTML stripped some content, see https://angular.dev/best-practices/security#preventing-cross-site-scripting-xss

Le premier insère l'attribut onerror, que le navigateur exécute quand l'image échoue à se charger. Le second affiche la saisie en texte dans le premier paragraphe, et ne garde dans le second que <b> et une image inoffensive. Les méthodes bypassSecurityTrust… se réservent au contenu que l'application produit elle-même ; les chercher dans le code est une revue de sécurité rapide.

Dépendances vulnérables

L'essentiel du code livré vient des dépendances, et leurs failles se publient après coup : un build d'hier peut devenir vulnérable aujourd'hui sans qu'une ligne change. Les deux écosystèmes comparent l'arbre des dépendances à une base d'avis de sécurité, et interrogent donc le réseau : les commandes qui suivent n'ont pas pu tourner ici, hors ligne.

<Project>
  <!-- Directory.Build.props, a la racine du depot : s'applique a tous les projets -->
  <PropertyGroup>
    <NuGetAuditMode>all</NuGetAuditMode>        <!-- le defaut des projets net10.0 -->
    <NuGetAuditLevel>moderate</NuGetAuditLevel> <!-- les avis low ne sont plus rapportes -->
    <!-- Avec TreatWarningsAsErrors : moderate reste un avertissement,
         high et critical font echouer la restauration -->
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsNotAsErrors>$(WarningsNotAsErrors);NU1902</WarningsNotAsErrors>
  </PropertyGroup>

  <ItemGroup>
    <!-- Un avis lu et juge sans objet ici ; l'URL vient de l'avertissement NU190x.
         Celle-ci est l'exemple de la documentation NuGet (NuGet.Protocol 5.11.2). -->
    <NuGetAuditSuppress Include="https://github.com/advisories/GHSA-g3q9-xf95-8hp5" />
  </ItemGroup>
</Project>
# .NET : la restauration audite deja ; ces commandes interrogent aussi la source.
dotnet package list --vulnerable --include-transitive   # avant .NET 10 : dotnet list package
dotnet package update --vulnerable                      # monte les paquets vulnerables

# npm : lit package-lock.json et soumet l'arbre au registre.
npm audit --audit-level=high   # echec a partir de high ; le rapport, lui, reste complet
npm audit --omit=dev           # seulement ce qui part en production
npm audit fix                  # mises a jour compatibles semver ; --force accepte les majeures
npm audit signatures           # signatures du registre et provenance des paquets installes

Côté .NET, l'audit a lieu à chaque restauration depuis le SDK 8 : c'est NuGetAudit, qui rapporte NU1901 à NU1904, de faible à critique. Depuis .NET 10, il couvre aussi les paquets transitifs des projets qui ciblent net10.0 ; avec TreatWarningsAsErrors, une faille publiée dans la dépendance d'une dépendance fait donc échouer la restauration. C'est souvent voulu ; sinon, WarningsNotAsErrors garde un niveau en avertissement, et NuGetAuditSuppress écarte un avis précis, une fois lu et jugé. Côté npm, npm audit sort en erreur à la moindre faille, et --audit-level n'en change que le seuil, pas le rapport ; npm audit fix --force accepte des versions majeures, donc des ruptures. Un avis ne prouve pas l'exploitabilité : la faille d'une dépendance de développement ne part pas, en général, dans le bundle livré, et --omit=dev restreint le rapport à ce qui part en production ; un outil de build compromis, lui, touche tout ce qu'il construit. Et un audit vide ne garantit rien : il dit seulement qu'aucun avis publié ne vise ces versions.

Ce cours vous a servi ? Offrir un café Signaler une erreur