Explainers

Règles Cloudflare WAF qui déclenchent des défis CAPTCHA

Un défi CAPTCHA Cloudflare n'apparaît jamais au hasard. Derrière chaque widget Turnstile ou page de vérification se cache une règle WAF, écrite par l'opérateur du site, qui a jugé votre requête suspecte selon un critère : adresse IP, pays, chemin, score de bot ou en-têtes. Savoir laquelle se déclenche, et pourquoi, vous permet de diagnostiquer le blocage et de choisir la bonne méthode de résolution.


Quelles actions de règle WAF produisent un CAPTCHA

Une règle WAF peut aboutir à plusieurs actions. Trois présentent un défi résoluble ; les autres laissent passer ou bloquent sans recours :

Action WAF Ce qui se passe Statut HTTP Méthode CaptchaAI
Managed Challenge Cloudflare choisit : défi invisible, Turnstile ou JS 503 turnstile
JS Challenge Page de défi JavaScript de 5 secondes 503 cloudflare_challenge
Interactive Challenge CAPTCHA classique (ancien, obsolète) 403 turnstile
Block 403 sec, aucun défi 403 N/A (non résoluble)
Allow Laisse passer, aucun contrôle 200 N/A
Skip Ignore les règles WAF suivantes 200 N/A
Log Journalise l'événement, aucune action 200 N/A

Le Managed Challenge, l'action la plus fréquente

Le Managed Challenge est l'action recommandée par Cloudflare, et la plus répandue. Plutôt que d'imposer un type de défi fixe, il l'adapte visiteur par visiteur :

WAF rule matches → Managed Challenge triggered
    ↓
Cloudflare evaluates visitor:
  ├─ Low risk → Invisible pass (no visible challenge)
  ├─ Medium risk → Turnstile widget (click to verify)
  └─ High risk → JavaScript challenge page
    ↓
Successful → cf_clearance cookie issued

Une même règle peut donc renvoyer un passage invisible un jour, une page JavaScript le lendemain, selon la réputation de votre IP au moment de la requête.


Les modèles de règles WAF qui piègent le trafic automatisé

Les opérateurs écrivent leurs règles avec le langage d'expression de Cloudflare. Trois familles ciblent l'identité de la requête : score de bot, pays et chemin.

Règles de score de bot

  • Ce qui déclenche : un score de bot sous le seuil fixé, souvent 30.
  • Premier déclencheur pour l'automatisation. Les solveurs de CaptchaAI utilisent de vrais navigateurs, d'où un score proche de l'humain.
# Challenge traffic with low bot scores
(cf.bot_management.score lt 30)
→ Action: Managed Challenge

# Challenge non-verified bots
(cf.bot_management.score lt 50 and not cf.bot_management.verified_bot)
→ Action: JS Challenge

Règles basées sur le pays

  • Ce qui déclenche : une IP dans un pays ciblé, ou hors de la liste autorisée.
# Challenge traffic from specific countries
(ip.geoip.country in {"CN" "RU" "VN" "IN"})
→ Action: Managed Challenge

# Block specific regions entirely
(ip.geoip.country eq "XX")
→ Action: Block

Règles basées sur le chemin

  • Ce qui déclenche : l'accès à des URL sensibles comme /login, /signup ou /api/.
# Challenge login page access
(http.request.uri.path eq "/login" or http.request.uri.path eq "/signup")
→ Action: Managed Challenge

# Challenge API endpoints
(http.request.uri.path contains "/api/")
→ Action: JS Challenge

Règles de fréquence, d'en-têtes et combinées

Ces trois modèles regardent comment vous requêtez.

Règles basées sur le taux

  • Ce qui déclenche : un volume de requêtes trop élevé, souvent couplé au threat_score.
# Challenge after high request rate
(cf.threat_score gt 10 and http.request.uri.path contains "/search")
→ Action: Managed Challenge

Règles basées sur les en-têtes

  • Ce qui déclenche : en-têtes absents ou révélateurs — Accept-Language manquant, User-Agent python ou curl.
# Challenge requests with no Accept-Language header
(not http.request.headers["accept-language"])
→ Action: JS Challenge

# Challenge requests with suspicious UA
(http.user_agent contains "python" or http.user_agent contains "curl")
→ Action: Managed Challenge

Règles composées

  • Ce qui déclenche : plusieurs conditions réunies (score faible et chemin /api/ et pays hors liste) — un filtre taillé pour le trafic automatisé.
# Multiple conditions
(cf.bot_management.score lt 30
 and http.request.uri.path contains "/api/"
 and ip.geoip.country ne "US")
→ Action: JS Challenge

Un cas concret côté francophone. Pendant les soldes, un site e-commerce active une règle de fréquence sur /checkout et /search. Vos tests QA, qui interrogeaient l'API de stock chaque seconde depuis une IP OVHcloud, se heurtent alors à un Managed Challenge : c'est le volume et l'origine datacenter qui l'ont déclenché. Côté RGPD, ne journalisez pas plus de données personnelles que nécessaire.


Identifier la règle qui s'est déclenchée

La réponse HTTP porte assez d'indices pour identifier la règle responsable.

À partir des en-têtes HTTP

import requests

def check_cloudflare_rule_info(url):
    """Extract WAF rule information from Cloudflare challenge response."""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 Chrome/120.0.0.0",
        "Accept": "text/html,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
    }

    response = requests.get(url, headers=headers, timeout=15, allow_redirects=False)

    info = {
        "status": response.status_code,
        "cf_ray": response.headers.get("cf-ray", ""),
        "cf_cache_status": response.headers.get("cf-cache-status", ""),
        "server": response.headers.get("server", ""),
    }

    # Challenge-specific info
    html = response.text

    if response.status_code == 503:
        if "jschl" in html:
            info["challenge_type"] = "JS Challenge (IUAM or WAF rule)"
        elif "challenge-platform" in html:
            info["challenge_type"] = "Managed Challenge"
        elif "cf-turnstile" in html:
            info["challenge_type"] = "Turnstile (Managed Challenge)"

    elif response.status_code == 403:
        if "cf-ray" in str(response.headers):
            info["challenge_type"] = "WAF Block (no challenge)"
        else:
            info["challenge_type"] = "Origin 403 (not Cloudflare)"

    return info

Le statut HTTP donne déjà la moitié de la réponse :

  • 503 : un défi résoluble, Managed ou JS Challenge.
  • 403 avec en-tête cf-ray : un blocage WAF sec, sans défi.
  • 403 sans cf-ray : la réponse vient de l'origine, pas de Cloudflare.

À partir du Ray ID Cloudflare

Chaque réponse Cloudflare porte un en-tête cf-ray. L'opérateur du site peut reporter ce Ray ID dans son tableau de bord (Security > Events) pour voir quelle règle s'est déclenchée. Vous n'avez pas cet accès de l'extérieur, mais noter le cf-ray de chaque défi vous aide à corréler vos observations dans le temps.


Résoudre un défi déclenché par le WAF

Le principe est simple : un Managed Challenge se résout avec la méthode turnstile après extraction du sitekey ; un JS Challenge passe par la méthode cloudflare_challenge avec le sitekey managed. Vous envoyez la tâche, puis interrogez le résultat toutes les 5 secondes jusqu'au token.

En Python

import requests
import time

API_KEY = "YOUR_API_KEY"

def solve_cloudflare_challenge(url, challenge_type):
    """Solve Cloudflare challenge based on WAF rule action."""

    if challenge_type == "managed_challenge":
        # Managed Challenge typically renders as Turnstile
        method = "turnstile"
        sitekey = extract_turnstile_sitekey(url)
    elif challenge_type == "js_challenge":
        # JavaScript Challenge page
        method = "cloudflare_challenge"
        sitekey = "managed"
    else:
        raise ValueError(f"Unknown challenge type: {challenge_type}")

    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": method,
        "sitekey": sitekey,
        "pageurl": url,
        "json": 1,
    })

    task_id = submit.json()["request"]

    for _ in range(60):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        }).json()

        if result.get("status") == 1:
            return result["request"]

    raise TimeoutError("Challenge solve timed out")


def extract_turnstile_sitekey(url):
    """Fetch page and extract Turnstile sitekey."""
    import re
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 Chrome/120.0.0.0",
    }
    response = requests.get(url, headers=headers, timeout=15)
    match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', response.text)
    return match.group(1) if match else None

Node.js

const axios = require("axios");

const API_KEY = "YOUR_API_KEY";

async function solveWAFChallenge(url, challengeType) {
  const method =
    challengeType === "js_challenge" ? "cloudflare_challenge" : "turnstile";
  const sitekey =
    challengeType === "js_challenge" ? "managed" : await extractSitekey(url);

  const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
    params: {
      key: API_KEY,
      method,
      sitekey,
      pageurl: url,
      json: 1,
    },
  });

  const taskId = submit.data.request;

  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));

    const result = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: taskId, json: 1 },
    });

    if (result.data.status === 1) {
      return result.data.request;
    }
  }

  throw new Error("Challenge solve timed out");
}

async function extractSitekey(url) {
  const response = await axios.get(url, {
    headers: {
      "User-Agent": "Mozilla/5.0 Chrome/120.0.0.0",
    },
  });
  const match = response.data.match(/data-sitekey=["']([0-9x][A-Za-z0-9_-]+)["']/);
  return match ? match[1] : null;
}

Une fois le token récupéré :

  • Réinjectez-le dans la requête vers la page protégée pour obtenir le cookie cf_clearance.
  • Il laisse passer vos requêtes suivantes tant que l'IP et le User-Agent ne changent pas.

Dépannage

Symptôme Règle WAF probable Correctif
Défi uniquement sur /login Règle de chemin Résoudre le défi pour ce chemin
Défi depuis des IP datacenter Score de bot ou réputation IP Proxys résidentiels ou résolution
Défi variable selon le pays Règle de pays Proxy dans un pays autorisé ou résolution
Défi après N requêtes Règle de taux Réduire la cadence ou résoudre chaque défi
Toujours un JS Challenge Action JS Challenge (non Managed) Méthode cloudflare_challenge
403 sans défi Action Block (non résoluble) Changer d'IP, d'en-têtes ou de requête

Suivre les changements de règles WAF

Les opérateurs ajustent régulièrement leurs règles ; chaque modification affecte directement vos automatisations :

Changement Effet sur l'automatisation Comment le détecter
Règle ajoutée Un défi sur des chemins qui passaient Bascules de statut 503/403
Règle supprimée Le défi disparaît 200 là où il y avait 503
Action durcie (Managed → Block) Un défi résoluble devient un blocage sec 403 au lieu de 503
Action assouplie (Block → Managed) Le blocage redevient résoluble 503 avec page de défi
Seuil modifié (score 30 → 50) Davantage de requêtes défiées Défis plus fréquents
Portée de chemin modifiée D'autres URL concernées De nouveaux chemins défiés

Surveiller l'évolution de la protection

import requests
import time

def monitor_cloudflare_protection(urls, interval=3600):
    """Monitor protection changes across URLs."""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 Chrome/120.0.0.0",
        "Accept": "text/html,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
    }

    last_status = {}

    while True:
        for url in urls:
            try:
                response = requests.get(
                    url, headers=headers, timeout=15, allow_redirects=False
                )
                status = response.status_code
                has_challenge = status == 503 or "cf-turnstile" in response.text

                current = {"status": status, "challenge": has_challenge}
                previous = last_status.get(url)

                if previous and current != previous:
                    print(f"[CHANGE] {url}")
                    print(f"  Before: {previous}")
                    print(f"  After:  {current}")

                last_status[url] = current

            except requests.RequestException as e:
                print(f"[ERROR] {url}: {e}")

        time.sleep(interval)

Cette sonde horaire vous alerte dès qu'un chemin bascule de 200 à 503 :

  • Vous repérez une règle ajoutée ou une action durcie.
  • Vous adaptez votre intégration avant une panne en série.

Questions fréquentes

Comment distinguer un Managed Challenge d'un JS Challenge ?

Les deux renvoient un statut 503, mais le HTML diffère : challenge-platform ou cf-turnstile indiquent un Managed Challenge, le marqueur jschl signale un JS Challenge.

CaptchaAI peut-il résoudre un blocage 403 renvoyé par une règle WAF ?

Non. Un 403 issu d'une action Block est un refus sec, sans défi à résoudre. La seule voie consiste à modifier ce qui a déclenché la règle : IP, en-têtes ou schéma de requête. Seuls les 503 exposent un défi exploitable.

Les proxys résidentiels réduisent-ils le nombre de défis ?

Souvent, oui. Beaucoup de règles ciblent la réputation IP ou l'origine datacenter. Un proxy résidentiel dans un pays autorisé abaisse la probabilité d'un défi sans l'éliminer : une règle de chemin ou de taux se déclenche quand même.

Environ 30 minutes après la résolution. Vos requêtes suivantes passent alors sans nouveau défi, à condition de conserver la même IP et le même User-Agent : le cookie est lié à ce couple.


En résumé

  • Une règle WAF déclenche un défi selon des conditions configurables : score de bot, pays, chemin, en-têtes ou taux.
  • L'action la plus courante, le Managed Challenge, s'affiche en défi invisible, Turnstile ou JavaScript.
  • Résolvez ces défis avec CaptchaAI via la méthode turnstile ou cloudflare_challenge.
  • Un blocage sec 403 ne se résout pas : revoyez votre schéma de requête ou votre IP.

Articles connexes

Les commentaires sont désactivés pour cet article.