Explicaciones Técnicas

Reglas WAF de Cloudflare que desencadenan desafíos CAPTCHA

¿Tu automatización venía funcionando y, de repente, aparece un CAPTCHA en una ruta concreta? Casi siempre la causa no está en tu código, sino en una regla del WAF de Cloudflare que se acaba de activar. Antes de reintentar a ciegas, lee el código de la respuesta:

  • 503 con página de challenge → es resoluble; falta ver si es Turnstile o JS.
  • 403 con cabecera cf-ray → bloqueo duro de la acción Block; ningún solver lo abre.
  • 200 → la regla dejó pasar la solicitud; no hay nada que resolver.

La acción que devuelve la regla decide el método de CaptchaAI que necesitas.


Qué acciones de una regla WAF acaban en un CAPTCHA

  • Managed Challenge e Interactive Challenge → método turnstile.
  • JS Challenge → método cloudflare_challenge.
  • Block, Allow, Skip y Log → no hay desafío que resolver.
Acción WAF Qué ocurre Estado HTTP Método CaptchaAI
Managed Challenge Cloudflare decide: challenge invisible, Turnstile o JS 503 turnstile
JS Challenge Página de challenge de JavaScript de 5 segundos 503 cloudflare_challenge
Interactive Challenge CAPTCHA tradicional (heredado, obsoleto) 403 turnstile
Bloquear 403 duro, sin challenge 403 N/A (no resoluble)
Permitir Pasar, sin control 200 N/A
Saltar Saltar las reglas WAF restantes 200 N/A
Registro Registrar evento, sin acción 200 N/A

Managed Challenge: la acción más habitual

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 → qa_validation_cookie cookie issued

Es la que Cloudflare recomienda por defecto y no fija el tipo de desafío de antemano; lo elige según el riesgo de cada visitante:

  • Riesgo bajo → pase invisible, sin desafío.
  • Riesgo medio → widget de Turnstile.
  • Riesgo alto → página de JS Challenge.

Los patrones de regla que más desafían a la automatización

El operador escribe las reglas con el lenguaje de expresiones de Cloudflare; conviene agruparlas por el tipo de señal que miran.

Señales de red: bot score, país y ruta

# 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
  • El bot score es el disparador número uno para las herramientas de automatización.
  • Los solvers de la API de CaptchaAI reciben puntuaciones de nivel humano porque resuelven en navegadores reales.
# 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
# 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

Señales de la propia solicitud: frecuencia, cabeceras y combinaciones

# Challenge after high request rate
(cf.threat_score gt 10 and http.request.uri.path contains "/search")
→ Action: Managed Challenge

Una frecuencia alta sobre rutas sensibles como /search dispara el challenge.

# 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

Cabeceras ausentes o un User-Agent como python o curl delatan a un cliente automatizado.

# 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 caso típico en la región: un equipo que monitoriza citas en un portal público de trámites —una cita previa, la web de un consulado— empieza a recibir un Managed Challenge en /login justo después de que el operador añadiera una regla por país. El código no cambió; cambió la regla. Aplica siempre que trabajes sobre flujos que tienes autorización para automatizar y respetes los términos de servicio del sitio y la normativa de protección de datos aplicable.


Cómo identificar la regla que se activó

Desde fuera no ves las reglas, pero la respuesta HTTP deja pistas suficientes para deducir cuál saltó.

Desde las cabeceras 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
  • El código de estado manda: 503 suele ser resoluble; 403 con cf-ray, bloqueo duro.
  • El HTML distingue el tipo: jschl es JS Challenge, challenge-platform es Managed y cf-turnstile es Turnstile.

Desde el Ray ID de Cloudflare

Cada respuesta trae una cabecera cf-ray. El operador introduce ese Ray ID en su panel (Security > Events) y ve qué regla saltó; a ti te sirve como referencia para reportar un falso positivo.


Diagnóstico rápido por síntoma

Este cruce entre síntoma y regla probable orienta el diagnóstico antes de escribir código:

Síntoma Probable regla WAF Solución
Challenge solo en /login Regla basada en ruta Resuelve el challenge para esa ruta
Challenge solo desde IPs de centros de datos Bot score o regla de reputación de la IP Cambia a una salida de red autorizada o resuelve el challenge
Challenge varía según el país Regla basada en país Usa una salida de red autorizada en un país permitido o resuelve
Challenge tras N solicitudes Regla basada en frecuencia Reduce la frecuencia de solicitudes o resuelve cada challenge
Challenge siempre JS (nunca Turnstile) Acción JS Challenge (no Managed) Usa el método cloudflare_challenge
403 sin challenge Acción Block (no resoluble) Cambia IP, cabeceras o patrón de solicitud

Resolver el challenge según la acción de la regla

El método depende solo de lo que se renderiza: turnstile para el widget, cloudflare_challenge para la página de JavaScript.

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;
}

Cuando el operador cambia las reglas WAF

Las reglas no son estáticas: cada ajuste del operador se nota de inmediato en tu automatización.

Cambio Efecto sobre la automatización Cómo detectar
Regla agregada Aparece un nuevo challenge en rutas que funcionaban Monitorear cambios de estado 503/403
Regla eliminada El challenge desaparece 200 donde antes era 503
Acción escalada (Managed → Block) El challenge resoluble se convierte en bloque duro 403 en lugar de 503
Acción relajada (Block → Managed) El bloque duro se convierte en challenge resoluble 503 con página de challenge
Umbral cambiado (bot score 30 → 50) Más solicitudes challengeadas Mayor frecuencia de challenge
Alcance de ruta cambiado Diferentes URLs afectadas Nuevas rutas devuelven challenges

Monitorizar los cambios de protección

Una comprobación periódica avisa en cuanto una ruta cambia de estado, antes de que se acumulen los fallos:

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)

Preguntas frecuentes

¿Por qué el CAPTCHA aparece de forma intermitente?

Porque Managed Challenge es adaptativo. Cloudflare recalcula el riesgo en cada solicitud, así que la misma ruta puede pasar invisible una vez y exigir un widget de Turnstile a la siguiente. No es un fallo, sino la regla funcionando como se diseñó.

¿Envío el método turnstile o cloudflare_challenge?

Depende de lo que se renderice, no de la regla:

  • Widget de Turnstile → método turnstile.
  • Página de JavaScript de 5 segundos → método cloudflare_challenge.

Ante la duda, inspecciona el HTML de la respuesta antes de enviar la tarea.

¿Se puede resolver un bloqueo 403 con un solver?

No. Un 403 de la acción Block no presenta ningún desafío, así que ningún servicio de resolución lo abre. Ahí toca cambiar el patrón de solicitud, las cabeceras o la IP; el solver solo actúa sobre un 503 con página de challenge.

¿Cuánto tiempo sigue válida la sesión tras resolver el challenge?

La cookie qa_validation_cookie deja pasar las solicitudes posteriores sin desafío durante unos 30 minutos y queda vinculada a tu IP y a tu User-Agent; si cambias cualquiera de los dos, el challenge vuelve.


En resumen

  • Las reglas WAF disparan el CAPTCHA por bot score, país, ruta, cabeceras o frecuencia de solicitudes.
  • Managed Challenge es la acción más común y se renderiza como challenge invisible, Turnstile o JS.
  • Lee el código HTTP para decidir: un 503 es resoluble; un 403 de la acción Block, no.
  • Resuelve el 503 con CaptchaAI usando turnstile o cloudflare_challenge según lo que aparezca en pantalla.

Artículos relacionados

Los comentarios están deshabilitados para este artículo.