Skip to main content
Cette page documente les protections techniques intégrées à Guardian Bot suite à l’audit de sécurité du 26 mars 2026. Elle s’adresse aux administrateurs qui souhaitent comprendre le fonctionnement interne du bot.

Vue d’Ensemble

Guardian Bot implémente une défense en profondeur (defense-in-depth) avec plusieurs couches indépendantes. Si une couche est contournée, les suivantes prennent le relais.
Message entrant


┌─────────────────────┐
│  Liste blanche       │ ◄── Domaines/IPs de confiance
└──────┬──────────────┘
       │ non-whitelisté

┌─────────────────────┐
│  Décompression safe  │ ◄── Protection contre les Decompression Bombs
└──────┬──────────────┘


┌─────────────────────┐
│  Résolution récursive│ ◄── Résolution des redirections (max 3 sauts)
└──────┬──────────────┘


┌─────────────────────┐
│  Scan multi-sources  │ ◄── PhishTank + GSB + VirusTotal
└──────┬──────────────┘
       │ menace détectée

┌─────────────────────┐
│  Trust Score & Action│ ◄── Sanction adaptée au score
└─────────────────────┘

1. Anti-Phishing Récursif

Problème adressé

Les attaquants utilisent des chaînes de redirections pour masquer les URLs malveillantes. Un scanner naïf vérifie bit.ly/xyz (inoffensif) mais ne voit jamais evil-phishing.com vers lequel il redirige.

Solution implémentée

Guardian résout récursivement chaque URL en suivant les redirections HTTP jusqu’à l’URL finale.
# Résolution avec protection contre les cycles et limite de profondeur
MAX_HOPS = 3
TIMEOUT_PER_HOP = 5  # secondes

async def resolve_redirects(url: str) -> tuple[str, list[str]]:
    visited = set()
    chain = [url]
    current = url

    for _ in range(MAX_HOPS):
        if current in visited:
            break  # Cycle détecté
        visited.add(current)

        response = await http_head(current, timeout=TIMEOUT_PER_HOP,
                                    allow_redirects=False)
        if response.status not in (301, 302, 303, 307, 308):
            break

        next_url = response.headers.get("Location")
        if not next_url:
            break

        current = normalize_url(next_url, base=current)
        chain.append(current)

    return current, chain

Traitement des domaines opaques

Certains domaines de redirection ne révèlent pas l’URL de destination sans interaction utilisateur (ex: get-qr.com, captcha gates). Guardian gère ce cas spécifiquement :
  • Résolution possible → scan de l’URL finale contre les bases de phishing
  • Résolution impossible (domaine opaque) → avertissement généré sans blocage automatique pour éviter les faux positifs
Les faux positifs sont prioritaires à éviter. Un avertissement sans blocage sur un domaine opaque est préférable à un blocage injustifié d’une URL légitime.

Détection de cycles

Un ensemble visited est maintenu pour chaque résolution. Si une URL apparaît deux fois dans la chaîne, la résolution s’arrête immédiatement pour éviter les boucles infinies.

2. Protection contre les Decompression Bombs

Problème adressé

Une Decompression Bomb est un fichier compressé de petite taille (quelques Ko) qui se décompresse en plusieurs gigaoctets. Si un attaquant envoie une image .zip ou un fichier encodé avec du contenu malveillant, un scanner naïf pourrait tenter de décompresser le contenu en mémoire et crasher le bot.

Solution implémentée

Guardian applique des limites strictes lors du traitement des pièces jointes :
MAX_FILE_SIZE = 10 * 1024 * 1024   # 10 Mo maximum
MAX_DECOMPRESSED_SIZE = 50 * 1024 * 1024  # 50 Mo limite décompression
MAX_IMAGE_PIXELS = 4096 * 4096     # 16 MP limite de rendu
Contrôles appliqués :
  1. Vérification de taille avant téléchargement : Le header Content-Length est vérifié. Si la taille dépasse MAX_FILE_SIZE, le fichier est ignoré sans téléchargement.
  2. Limite de pixels pour les images QR : Avant de passer une image au décodeur QR, les dimensions sont vérifiées. Une image de 1px × 4 milliards de pixels serait refusée.
  3. Timeout de décompression : Chaque opération de décompression s’exécute dans un contexte avec timeout (asyncio.wait_for). Si la décompression dépasse la limite de temps, l’opération est annulée.
  4. Isolation des erreurs : Les exceptions de décompression sont capturées localement et logguées sans faire crasher le worker principal.

3. Protection Anti-Spam des APIs Externes

Problème adressé

Guardian consulte jusqu’à 3 APIs externes (PhishTank, Google Safe Browsing, VirusTotal) par URL scannée. Sans limitation, un attaquant pourrait envoyer des milliers de messages contenant des URLs pour épuiser les quotas API ou surcharger le bot.

Solution implémentée

Un asyncio.Semaphore limite le nombre de requêtes API simultanées :
# Antiraid + scan concurrent limité
api_semaphore = asyncio.Semaphore(5)  # Max 5 requêtes parallèles

async def scan_url_with_ratelimit(url: str) -> ScanResult:
    async with api_semaphore:
        return await _scan_url_internal(url)
Mécanismes combinés :
MécanismeImplémentationObjectif
Semaphore globalSemaphore(5)Limite les appels API simultanés
Cache de résultatsTTL 1h par URLÉvite de scanner la même URL deux fois
Trust Score probabilisteScore × 0.3Réduit les scans sur membres de confiance
Whitelist préemptiveVérification avant réseauCourt-circuite tous les scans
Timeout par requête5s max par hopEmpêche les attentes infinies

Scan probabiliste

Pour éviter de scanner chaque URL envoyée par un membre de confiance, Guardian applique un échantillonnage probabiliste basé sur le Trust Score :
Probabilité de scan = max(0.1, 1.0 - (trust_score / 100) * 0.7)
Un membre avec un score de 90 n’a que 37% de probabilité d’être scanné sur chaque message. Un membre avec un score de 10 est scanné 93% du temps.

4. Protection contre les Fuites Mémoire

Problème adressé

Les sessions longues (captcha, anti-spam) accumulent des données en mémoire si elles ne sont pas nettoyées. Un attaquant peut créer des milliers de sessions captcha non terminées pour épuiser la RAM du bot.

Solution implémentée

Des tâches de nettoyage périodiques tournent en arrière-plan pour chaque cog concerné :
# Exemple dans captcha.py
@tasks.loop(minutes=10)
async def cleanup_expired_sessions(self):
    now = asyncio.get_event_loop().time()
    expired = [
        session_id
        for session_id, session in self.active_sessions.items()
        if now - session.created_at > SESSION_TIMEOUT
    ]
    for session_id in expired:
        del self.active_sessions[session_id]
Cogs avec nettoyage automatique :
CogDonnées nettoyéesIntervalle
captcha.pySessions captcha expirées10 minutes
automod.pyHistorique de messages anti-spam5 minutes
report.pyCooldowns de signalement expirés30 minutes
antiraid.pyFenêtres de détection de raid1 minute

5. Isolation de la Base de Données

Problème adressé

Les accès directs à la base de données depuis plusieurs cogs simultanés peuvent créer des conditions de course et des connexions pendantes en cas d’erreur.

Solution implémentée

Toutes les requêtes passent par un pool centralisé avec gestion de contexte :
# utils/database.py
class Database:
    def __init__(self, pool):
        self._pool = pool  # Accès restreint via propriété

    @property
    def pool(self):
        return self._pool

    async def fetch_one(self, query: str, *args):
        async with self._pool.acquire() as conn:
            return await conn.fetchrow(query, *args)
Les requêtes indépendantes sont parallélisées avec asyncio.gather pour réduire la latence :
# Chargement parallèle au lieu de séquentiel
trust_score, warnings, infractions = await asyncio.gather(
    db.fetch_trust_score(guild_id, user_id),
    db.fetch_warnings(guild_id, user_id),
    db.fetch_infractions(guild_id, user_id)
)

6. Vérification par Captcha

Algorithme

Le captcha mathématique utilise secrets.choice (CSPRNG) au lieu de random pour éviter la prédiction de réponses :
import secrets

def generate_captcha() -> tuple[str, int]:
    a = secrets.choice(range(1, 20))
    b = secrets.choice(range(1, 20))
    op = secrets.choice(['+', '-', '×'])
    # ...
    return question, correct_answer
Un asyncio.Lock par session empêche les conditions de course si l’utilisateur clique plusieurs fois simultanément.

Limites

  • 3 tentatives maximum par session
  • Timeout de 5 minutes par session
  • Expiration automatique : les sessions non terminées sont nettoyées toutes les 10 minutes
  • Résultat : +15 Trust Score (succès) ou -20 Trust Score + expulsion (échec)

7. Hiérarchie et Prévention des Escalades

Guardian vérifie systématiquement la hiérarchie des rôles avant toute action modération :
Vérification avant ban/kick/mute :
  1. La cible est-elle un bot ? → Refus
  2. La cible est-elle le propriétaire du serveur ? → Refus
  3. Le rôle le plus haut de la cible ≥ rôle le plus haut du bot ? → Refus
  4. Le rôle le plus haut de la cible ≥ rôle le plus haut du modérateur ? → Refus
  5. Action autorisée ✓

Détection d’abus de modérateurs

Si un modérateur effectue trop d’actions en peu de temps (seuil configurable, défaut : 3 actions/10s), Guardian :
  1. Logue l’événement comme suspect
  2. Notifie les administrateurs
  3. Peut restreindre les permissions du compte modérateur si le seuil est dépassé

8. Global Ban et Score de Confiance

Chaque entrée dans la liste noire globale porte un score de confiance :
CatégorieScore de confianceDescription
scammer90%Arnaqueur confirmé
raider85%Raideur identifié
spammer75%Spammeur documenté
other60%Catégorie générique
Les serveurs peuvent configurer un seuil minimal de confiance en dessous duquel le ban automatique à l’arrivée n’est pas déclenché, évitant les faux positifs sur des entrées peu fiables.

Résumé des Paramètres de Sécurité

ProtectionParamètre cléValeur par défaut
Redirections maxMAX_HOPS3
Timeout par hopTIMEOUT_PER_HOP5s
Taille fichier maxMAX_FILE_SIZE10 Mo
Pixels image maxMAX_IMAGE_PIXELS16 MP (4096²)
Semaphore APIapi_semaphore5 parallèles
Cache scan URLTTL1 heure
Sessions captcha max3 tentatives / 5 min
Nettoyage sessionsIntervalle10 minutes
Seuil spam (défaut)messages/fenêtre5 msgs / 5s
Itérations PBKDF2Sauvegardes480 000