هل ظهر تحدي Cloudflare فجأة على مسار كان يعمل بالأمس؟ غالباً لم يتغيّر سكربتك، بل تغيّرت قاعدة في جدار حماية تطبيقات الويب (WAF). يربط WAF إجراءً — مثل عرض تحدٍّ — بسمات الطلب: عنوان IP، أو البلد، أو المسار، أو درجة البوت، أو الرؤوس. ومتى عرفت القاعدة التي طابقت طلبك، عرفت سبب التحدي وطريقة التعامل معه.
- حدّد الإجراء: تحدٍّ قابل للحل (503) أم حظر صارم (403)؟
- اعرف نوع التحدي: Managed Challenge أم JS Challenge؟
- اختر الطريقة:
turnstileأوcloudflare_challengeبحسب ما يُعرَض.
إجراءات WAF التي تُنتج تحديات CAPTCHA
تدعم قواعد WAF عدة إجراءات، لكن ثلاثة منها فقط تُنتج تحديات قابلة للحل برمجياً؛ أما البقية فتُنهي الطلب:
| إجراء WAF | ما الذي يحدث | حالة HTTP | طريقة CaptchaAI |
|---|---|---|---|
| Managed Challenge | تختار Cloudflare تلقائياً: تمرير غير مرئي أو Turnstile أو تحدي JS | 503 | turnstile |
| JS Challenge | صفحة تحدي JavaScript مدتها نحو 5 ثوانٍ | 503 | cloudflare_challenge |
| Interactive Challenge | اختبار CAPTCHA تقليدي (إرث قديم مُهمَل) | 403 | turnstile |
| Block | حظر صريح 403 دون تحدٍّ | 403 | غير قابل للحل |
| Allow | مرور مباشر دون فحص | 200 | لا ينطبق |
| Skip | تخطّي بقية قواعد WAF | 200 | لا ينطبق |
| Log | تسجيل الحدث فقط دون إجراء | 200 | لا ينطبق |
الأكثر شيوعاً هو Managed Challenge الموصى به من Cloudflare، إذ يقرّر نوع التحدي تكيّفياً لكل زائر بحسب درجة المخاطرة، فلا يُثقِل الزوار الموثوقين بتحديات مرئية:
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
مثال تطبيقي: متجر خليجي في موسم التخفيضات
تخيّل فريق بيانات في متجر تجزئة خليجي يجمع أسعار المنافسين عبر سكربت يومي على صفحات /search. مع عروض «الجمعة البيضاء» يشدّد مشغّل الموقع قواعد WAF، فيبدأ السكربت فجأة باستقبال حالة 503 مع Managed Challenge. الحل ليس تعديل الكود — فالسبب خارجي — بل تمرير التحدي إلى خدمة حل عبر الـ API. ولأن نموذج CaptchaAI يعتمد على الـ threads لا على عدد عمليات الحل، تمنح خطة ADVANCE ($90 شهرياً، 50 thread) سعةً لتمرير تحديات متزامنة كثيرة دون رسوم لكل عملية.
أنماط قواعد WAF الشائعة
يكتب مشغّلو المواقع قواعد WAF بلغة التعبير الخاصة بـ Cloudflare. وأكثر الأنماط إطلاقاً للتحديات أمام حركة المرور الآلية تندرج في ثلاث عائلات: هوية الطلب، والوجهة، والبصمة.
قواعد درجة البوت والبلد
تُطلِق هذه القواعد التحدي بحسب درجة ثقة الزائر أو موقعه الجغرافي، وهي الأشيع أمام أدوات الأتمتة. وتنال أدوات الحل في CaptchaAI درجات قريبة من درجات البشر لأنها تعمل داخل متصفحات حقيقية:
# 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
# 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
قواعد المسار ومعدل الطلبات
تستهدف هذه القواعد مسارات حسّاسة مثل /login و/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
# Challenge after high request rate
(cf.threat_score gt 10 and http.request.uri.path contains "/search")
→ Action: Managed Challenge
قواعد الرؤوس والشروط المركّبة
تكشف قواعد الرؤوس أدوات الأتمتة البسيطة التي تُرسل طلبات بلا رأس Accept-Language أو بوكيل مستخدم يحمل python أو 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
# Multiple conditions
(cf.bot_management.score lt 30
and http.request.uri.path contains "/api/"
and ip.geoip.country ne "US")
→ Action: JS Challenge
كيف تحدّد القاعدة التي أطلقت التحدي
عند ظهور التحدي يمكنك استنتاج القاعدة المسؤولة من الاستجابة نفسها، عبر مصدرين:
من رؤوس 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
من معرّف Cloudflare Ray
تتضمّن كل استجابة من Cloudflare رأس cf-ray. ويستطيع مشغّل الموقع تتبّع القاعدة الدقيقة عبر هذا المعرّف بثلاث خطوات:
- انسخ قيمة
cf-rayمن رؤوس الاستجابة. - افتح لوحة تحكم Cloudflare وانتقل إلى «Security > Events».
- ابحث عن المعرّف لتعرف القاعدة التي طابقت والإجراء الذي اتُّخذ.
حلّ التحديات الناتجة عن قواعد WAF
بعد تحديد نوع التحدي يصبح الحل مسألة اختيار الطريقة: turnstile لتحديات Managed، وcloudflare_challenge لصفحة تحدي JavaScript. والمسار واحد: أرسِل المهمة إلى in.php ثم استطلع النتيجة من res.php.
استراتيجية تعتمد على نوع التحدي
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;
}
تغييرات قواعد WAF وأثرها على الأتمتة
يضبط مشغّلو المواقع قواعد WAF باستمرار، ولكل تغيير أثر مباشر على استقرار الأتمتة وطريقة رصده:
| التغيير | الأثر على الأتمتة | كيف تكتشفه |
|---|---|---|
| إضافة قاعدة | يظهر تحدٍّ جديد على مسارات كانت تعمل | راقب تحوّل الحالة إلى 503/403 |
| إزالة قاعدة | يختفي التحدي | ظهور 200 حيث كان 503 سابقاً |
| تصعيد الإجراء (من Managed إلى Block) | يتحوّل التحدي القابل للحل إلى حظر صارم | 403 بدل 503 |
| تخفيف الإجراء (من Block إلى Managed) | يتحوّل الحظر الصارم إلى تحدٍّ قابل للحل | 503 مع صفحة تحدٍّ |
| تغيير العتبة (درجة البوت من 30 إلى 50) | تُطلَب تحديات لعدد أكبر من الطلبات | ارتفاع وتيرة التحديات |
| تغيير نطاق المسار | تتأثر عناوين URL مختلفة | مسارات جديدة تُعيد التحديات |
استراتيجية الرصد
راقب المسارات الحسّاسة دورياً لتكتشف أي تحوّل في الحماية مبكراً. يقارن السكربت التالي حالة كل عنوان URL بما كانت عليه ويسجّل أي تغيّر:
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)
استكشاف الأخطاء وإصلاحها
طابِق العَرَض الظاهر مع القاعدة الأرجح ثم اختر الإصلاح المناسب:
| العَرَض | قاعدة WAF المحتملة | الحل |
|---|---|---|
تحدٍّ على /login فقط |
قاعدة قائمة على المسار | حُلّ التحدي لهذا المسار |
| تحدٍّ من عناوين مراكز البيانات فقط | قاعدة درجة البوت أو سمعة عنوان IP | استخدم وكلاء سكنيين أو حُلّ التحدي |
| تحدٍّ يتغيّر بحسب البلد | قاعدة قائمة على البلد | استخدم وكيلاً في بلد مسموح |
| تحدٍّ بعد عدد N من الطلبات | قاعدة قائمة على المعدل | خفّض معدل الطلبات أو حُلّ كل تحدٍّ |
| تحدٍّ دائماً من نوع JS | إجراء JS Challenge وليس Managed | استخدم طريقة cloudflare_challenge |
| 403 دون أي تحدٍّ | إجراء Block (غير قابل للحل) | غيّر عنوان IP أو الرؤوس أو نمط الطلب |
الأسئلة الشائعة
لماذا يظهر تحدي Cloudflare فجأة على صفحة كانت تعمل؟
غالباً أضاف مشغّل الموقع قاعدة جديدة، أو خفض عتبة درجة البوت، أو صعّد إجراء قاعدة قائمة. راقب تحوّل الاستجابة من 200 إلى 503/403 لتتأكد أن السبب في القاعدة لا في سكربتك.
ما الفرق بين تحدي 503 وحظر 403؟
- 503: تحدٍّ قابل للحل (Managed أو JS Challenge) يمكن تمريره عبر الـ API.
- 403 من إجراء Block: حظر صارم لا يُحل؛ غيّر عنوان IP أو الرؤوس أو نمط الطلب.
هل يمكنني منع ظهور التحديات بدل حلّها؟
- استخدم وكلاء سكنيين لتحسين سمعة عنوان IP.
- أرسل رؤوس متصفح كاملة وخفّض معدل الطلبات لتفادي قواعد المعدل.
- تذكّر أن قواعد المسار أو البلد قد تُطلق التحدي مهما فعلت، ويبقى الحل عبر الـ API الأكثر موثوقية.
هل يكفي حل التحدي مرة واحدة أم يتكرر مع كل طلب؟
بعد الحل تُصدر Cloudflare ملف تعريف الارتباط cf_clearance الذي يمرّر الطلبات اللاحقة دون تحدٍّ لنحو 30 دقيقة. لكنه مرتبط بعنوان IP ووكيل المستخدم معاً، فأي تغيير في أحدهما يُبطله ويعيد التحدي.
الخلاصة
تُطلق قواعد Cloudflare WAF تحديات CAPTCHA وفق شروط قابلة للضبط: درجة البوت، أو البلد، أو المسار، أو الرؤوس، أو المعدل. والإجراء الأشيع هو Managed Challenge التكيّفي. وتبقى القاعدة بسيطة:
- تحديات 503 قابلة للحل عبر CaptchaAI بطريقة
turnstileأوcloudflare_challenge. - حظر 403 الصارم غير قابل للحل؛ غيّر نمط الطلب أو عنوان IP.
- أي تغيّر مفاجئ في السلوك سببه غالباً تعديل في قاعدة WAF، لا عطل في سكربتك.
مقالات ذات صلة
- الفرق بين Cloudflare Challenge وTurnstile وكيفية تمييزهما
- كيف يعمل Cloudflare Challenge خطوة بخطوة
- جولة في تدفّق جلسة Cloudflare Challenge