API Tutorials

Paramètres de la page Cloudflare Challenge et flux de jetons

Un cookie cf_clearance valide : voilà ce que produit, au bout de la chaîne, une page Cloudflare Challenge. Entre la première réponse HTTP 503 et ce cookie, Cloudflare enchaîne des paramètres de page, un script obfusqué et une preuve de travail exécutée en JavaScript. Savoir lire ces paramètres, c'est pouvoir dire pourquoi une résolution échoue — et quand la déléguer à un solveur.

Ce guide suit l'ordre du flux :

  • les éléments de la page de défi et leur rôle ;
  • le parcours du token jusqu'au cookie cf_clearance ;
  • comment inspecter ces paramètres, puis laisser CaptchaAI résoudre.

Ce que contient une page Cloudflare Challenge

Une page de défi Cloudflare (HTTP 503) réunit plusieurs éléments qui pilotent la suite du flux :

<!DOCTYPE html>
<html>
<head>
    <title>Just a moment...</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
    <div id="challenge-stage">
        <div id="challenge-body-text">
            Checking if the site connection is secure
        </div>
        <div id="challenge-spinner">
            <!-- Loading spinner -->
        </div>
    </div>

    <div id="challenge-form" style="display:none">
        <form id="challenge-form" action="/..." method="POST">
            <!-- Hidden parameters -->
            <input type="hidden" name="md" value="...">
            <input type="hidden" name="r" value="...">
        </form>
    </div>

    <script src="/cdn-cgi/challenge-platform/h/g/orchestrate/chl_page/v1?ray=...">
    </script>
</body>
</html>

Les paramètres clés à connaître

Trois familles cohabitent : les paramètres du HTML, ceux de l'URL du script et ceux chargés par le JavaScript. D'abord, ceux de la page :

  • ray — Ray ID Cloudflare : relie le défi à la requête d'origine.
  • md — métadonnées du défi : état chiffré.
  • r — token de réponse : calculé par le JavaScript.
  • chl_opt — options passées au script de défi.
  • cRay — Ray secondaire, pour le suivi.
  • cZone — ID de zone Cloudflare.
  • cUPMDTk — horodatage d'émission du défi.
  • cHash — hash de contrôle d'intégrité.

L'URL du script de défi encode elle aussi de l'information :

/cdn-cgi/challenge-platform/h/g/orchestrate/chl_page/v1?ray=ABC123

Décomposée, elle se lit ainsi :

  • /cdn-cgi/challenge-platform/ — infrastructure de défi Cloudflare ;
  • h/g/ — version ou variante du défi ;
  • orchestrate/ — endpoint d'orchestration ;
  • chl_page/v1 — version de la page ;
  • ray=ABC123 — Ray ID lié à la requête.

Une fois exécuté, le script charge un objet d'options qui porte le type du défi et le temps d'attente minimal :

// Extracted from obfuscated challenge script
window._cf_chl_opt = {
    cvId: '2',           // Challenge version
    cType: 'managed',    // Challenge type
    cNounce: '...',      // Cryptographic nonce
    cRay: '...',         // Challenge Ray ID
    cHash: '...',        // Challenge hash
    cUPMDTk: '...',      // Timestamp
    cFPWv: 'g',          // Fingerprint version
    cTTimeMs: '4000',    // Minimum wait time (ms)
    cTplV: 5,            // Template version
    cLt: '...',          // Challenge lifetime
    cRq: {},             // Challenge request data
};

Du défi au cf_clearance : le flux de bout en bout

Cloudflare répond d'abord par un 503 et pose un cookie __cf_bm ; le navigateur exécute le script, calcule la preuve de travail et renvoie le token ; le cf_clearance n'est émis qu'ensuite :


1. CLIENT → CLOUDFLARE EDGE
   GET /protected-page
   ↓

2. CLOUDFLARE → CLIENT
   HTTP 503 + Challenge page HTML
   Sets: __cf_bm cookie (bot management tracking)
   Contains: ray ID, challenge script URL
   ↓

3. CLIENT (browser)
   Loads challenge script from /cdn-cgi/challenge-platform/...
   ↓

4. CHALLENGE SCRIPT EXECUTES:
   a. Collects browser fingerprint:

      - Canvas hash
      - WebGL renderer
      - Screen dimensions
      - Installed fonts
      - Timezone
      - Language
   b. Runs proof-of-work:

      - Iterates hash computations
      - Must find answer matching difficulty
   c. Computes timing:

      - Enforces minimum wait (cTTimeMs)
      - Records actual timing
   d. Generates response token:

      - Combines fingerprint + PoW answer + timing
      - Encrypts with challenge nonce
   ↓

5. CLIENT → CLOUDFLARE
   POST /cdn-cgi/challenge-platform/h/g/flow/ov1/...
   Body: { r: "encrypted_response", md: "metadata", ... }
   ↓

6. CLOUDFLARE validates:
   - Proof-of-work answer correct?
   - Timing within acceptable range?
   - Fingerprint consistent with real browser?
   - No replay (nonce check)?
   ↓

7. CLOUDFLARE → CLIENT
   HTTP 200 + Set-Cookie: cf_clearance=...; path=/; expires=...
   + HTTP redirect to original URL
   ↓

8. CLIENT → CLOUDFLARE
   GET /protected-page
   Cookie: cf_clearance=...
   ↓

9. CLOUDFLARE → CLIENT
   HTTP 200 + Protected content

Côté cookies, la séquence se résume ainsi : suivi posé d'emblée, autorisation obtenue après résolution, puis réémission du défi à l'expiration :

Request 1: No cookies
    → Challenge page (503)
    → __cf_bm cookie set

Challenge solve:
    → cf_clearance cookie set

Request 2+: cf_clearance + __cf_bm
    → Content served (200)

After ~30 mins: cf_clearance expires
    → Next request triggers new challenge

Les cookies posés par Cloudflare

Quatre cookies interviennent, un seul porte l'autorisation :

Cookie Rôle Durée de vie Portée
__cf_bm Suivi de session de gestion du trafic automatisé 30 minutes Domaine
cf_clearance Preuve de résolution du défi 15 min – 24 h (configurable) Domaine
__cflb Affinité de l'équilibreur de charge Session Domaine
_cfuvid Identifiant unique du visiteur Session Domaine

Le cookie cf_clearance n'est pas transférable. Il est verrouillé sur trois éléments :

  • Adresse IP – il doit provenir de la même IP que celle qui a résolu le défi ;
  • User-Agent – il doit correspondre à l'UA utilisé pendant la résolution ;
  • Domaine – il n'est valable que pour le domaine émetteur.

Exemple concret : une équipe QA teste son site depuis un worker en eu-west-3 (Paris) ; si la rotation de proxy change d'IP entre la résolution et la requête suivante, le cookie est aussitôt invalidé.

# ❌ FAILS — IP mismatch
# Solve challenge from IP A, then use cf_clearance from IP B

# ❌ FAILS — UA mismatch
# Solve with Chrome UA, then send requests with Firefox UA

# ✅ WORKS — Same IP + Same UA
session = requests.Session()
session.headers["User-Agent"] = "Mozilla/5.0 ... Chrome/120.0.0.0"
# Use same session for solving and subsequent requests

Côté RGPD : un cf_clearance est un identifiant de session lié à une IP. Si vous le journalisez, limitez sa rétention dans vos logs.


Extraire les paramètres pour diagnostiquer

Inspecter une page de défi confirme son type et le script chargé. En Python :

import re
import requests

def extract_challenge_params(url):
    """Extract Cloudflare challenge page parameters."""
    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)
    html = response.text

    params = {
        "status_code": response.status_code,
        "cf_ray": response.headers.get("cf-ray", ""),
        "is_challenge": response.status_code == 503,
    }

    if not params["is_challenge"]:
        return params

    # Extract Ray ID from page
    ray_match = re.search(r"ray['\"]?\s*[:=]\s*['\"]([a-f0-9]+)['\"]", html, re.I)
    if ray_match:
        params["ray_id"] = ray_match.group(1)

    # Extract challenge script URL
    script_match = re.search(
        r'src=["\'](/cdn-cgi/challenge-platform/[^"\']+)["\']', html
    )
    if script_match:
        params["challenge_script"] = script_match.group(1)

    # Extract challenge options
    opt_match = re.search(r"_cf_chl_opt\s*=\s*\{([^}]+)\}", html)
    if opt_match:
        opt_text = opt_match.group(1)

        # Parse individual options
        for key in ["cType", "cRay", "cHash", "cTTimeMs", "cvId", "cFPWv"]:
            val_match = re.search(
                rf"{key}\s*:\s*['\"]?([^'\"', }}]+)", opt_text
            )
            if val_match:
                params[key] = val_match.group(1)

    # Extract form parameters
    md_match = re.search(r'name=["\']md["\']\s+value=["\']([^"\']+)["\']', html)
    if md_match:
        params["md"] = md_match.group(1)

    # Extract cookies from response
    params["cookies"] = {
        name: value
        for name, value in response.cookies.items()
    }

    return params


# Usage
params = extract_challenge_params("https://protected-site.com")
if params["is_challenge"]:
    print(f"Challenge type: {params.get('cType', 'unknown')}")
    print(f"Ray ID: {params.get('ray_id', params['cf_ray'])}")
    print(f"Min wait: {params.get('cTTimeMs', '?')}ms")
    print(f"Script: {params.get('challenge_script', 'not found')}")

La même inspection en Node.js :

const axios = require("axios");

async function extractChallengeParams(url) {
  const response = await axios.get(url, {
    headers: {
      "User-Agent": "Mozilla/5.0 Chrome/120.0.0.0",
      Accept: "text/html,*/*;q=0.8",
    },
    validateStatus: () => true,
    maxRedirects: 0,
  });

  const html = response.data;
  const params = {
    statusCode: response.status,
    cfRay: response.headers["cf-ray"] || "",
    isChallenge: response.status === 503,
  };

  if (!params.isChallenge) return params;

  // Extract challenge script URL
  const scriptMatch = html.match(
    /src=["'](\/cdn-cgi\/challenge-platform\/[^"']+)["']/
  );
  if (scriptMatch) params.challengeScript = scriptMatch[1];

  // Extract challenge type
  const typeMatch = html.match(/cType\s*:\s*['"]?(\w+)/);
  if (typeMatch) params.challengeType = typeMatch[1];

  // Extract timing
  const timeMatch = html.match(/cTTimeMs\s*:\s*['"]?(\d+)/);
  if (timeMatch) params.minWaitMs = parseInt(timeMatch[1]);

  return params;
}

extractChallengeParams("https://protected-site.com").then(console.log);

Déléguer tout le flux à CaptchaAI

L'extraction manuelle sert au diagnostic, mais reproduire la preuve de travail et l'empreinte en production n'est pas tenable. CaptchaAI gère tout le flux en interne — vous n'avez rien à extraire :

import requests
import time

API_KEY = "YOUR_API_KEY"

def solve_cloudflare_challenge(target_url):
    """Solve Cloudflare challenge page — CaptchaAI handles token flow."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "cloudflare_challenge",
        "sitekey": "managed",
        "pageurl": target_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")


# CaptchaAI handles the full flow:
# 1. Loads the challenge page
# 2. Executes JavaScript
# 3. Solves proof-of-work
# 4. Returns clearance token/cookies
token = solve_cloudflare_challenge("https://protected-site.com/login")

Le Cloudflare Challenge est pris en charge en version GA via la méthode cloudflare_challenge. La facturation est basée sur les threads : sur un plan BASIC ($15/mois, 5 threads), chaque thread traite un défi à la fois, sans coût par résolution. Pensez à réutiliser la même IP et le même User-Agent après l'obtention du cookie.


Diagnostiquer un défi qui échoue

Quand la résolution ne passe pas, la panne se situe presque toujours à l'un de ces points de rupture :

  • La page ne se charge pas — timeout ou réponse vide : problème réseau ou proxy.
  • Le script ne s'exécute pas — boucles de défi : API JavaScript manquantes.
  • La preuve de travail échoue — spinner infini : timeout de calcul.
  • Réponse rejetée — retour au défi : timing invalide ou empreinte incohérente.
  • cf_clearance non posé — cookie absent : erreur d'analyse de la réponse.
  • cf_clearance rejeté — 403 ensuite : incompatibilité d'IP ou d'UA.

Cette fonction vérifie le flux étape par étape, avec ou sans cookie cf_clearance :

def debug_challenge_flow(url, cf_clearance_cookie=None, user_agent=None):
    """Debug the challenge solve flow step by step."""
    ua = user_agent or (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 Chrome/120.0.0.0"
    )

    steps = []

    # Step 1: Initial request
    response = requests.get(
        url,
        headers={"User-Agent": ua, "Accept": "text/html,*/*;q=0.8"},
        timeout=15,
        allow_redirects=False,
    )
    steps.append({
        "step": "initial_request",
        "status": response.status_code,
        "is_challenge": response.status_code == 503,
        "cf_ray": response.headers.get("cf-ray", ""),
    })

    # Step 2: Test with cf_clearance
    if cf_clearance_cookie:
        session = requests.Session()
        session.cookies.set("cf_clearance", cf_clearance_cookie)
        session.headers["User-Agent"] = ua

        response2 = session.get(url, timeout=15, allow_redirects=False)
        steps.append({
            "step": "with_clearance",
            "status": response2.status_code,
            "passed": response2.status_code == 200,
        })

        if response2.status_code != 200:
            steps.append({
                "step": "diagnosis",
                "issue": "cf_clearance rejected",
                "possible_causes": [
                    "Cookie expired",
                    "IP address changed",
                    "User-Agent mismatch",
                    "Cookie from different domain",
                ],
            })

    return steps

Dépannage

Les symptômes les plus fréquents et leur correctif :

Symptôme Cause Correctif
Le type de défi est « managed » mais la résolution échoue Le défi attend Turnstile, pas un défi JS Essayez la méthode turnstile au lieu de cloudflare_challenge
cf_clearance fonctionne une fois, puis est rejeté La rotation de proxy a changé votre IP Épinglez l'IP pendant toute la durée de vie de l'autorisation
La page « Just a moment... » ne se résout jamais JavaScript bloqué ou mal formé Passez par CaptchaAI plutôt qu'une résolution manuelle
Le défi réapparaît à chaque requête Le cf_clearance n'est pas renvoyé Vérifiez que les cookies sont conservés dans la session
Un défi différent selon le chemin Règles WAF par chemin Résolvez séparément pour chaque chemin

Questions fréquentes

Les questions les plus fréquentes à l'intégration :

CaptchaAI prend-il en charge le Cloudflare Challenge managé ?

Oui, en version GA via la méthode cloudflare_challenge. Le solveur charge la page, exécute le JavaScript, calcule la preuve de travail et renvoie le token d'autorisation.

Presque toujours à cause de l'un de ces écarts entre la résolution et la requête protégée :

  • l'adresse IP a changé (rotation de proxy, réseau différent) ;
  • le User-Agent ne correspond plus à celui de la résolution ;
  • le cookie a expiré ou provient d'un autre domaine.

Faut-il un proxy résidentiel pour conserver cf_clearance ?

Pas nécessairement, mais l'IP doit rester stable pendant toute la durée de vie du cookie ; une rotation de proxy en cours de session invalide l'autorisation.

Le Cloudflare Challenge et Turnstile, est-ce la même chose ?

Non — ce sont deux mécanismes distincts :

  • le Cloudflare Challenge est une page interstitielle (« Just a moment... ») avec preuve de travail ;
  • Turnstile est un widget intégré à un formulaire, avec sa propre méthode d'API.

En résumé

Une page Cloudflare Challenge combine un Ray ID, un script de défi, un objet d'options et des paramètres de formulaire qui alimentent un flux à preuve de travail. Le résultat est un cookie cf_clearance lié à l'IP et au User-Agent, valable de 15 minutes à 24 heures. Avec CaptchaAI, vous n'analysez rien à la main : le solveur gère tout le flux. Et quand un cas échoue, ces paramètres vous disent où la chaîne s'est rompue.

Articles connexes

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