4.6 KiB
Cloudflare Turnstile — automation barriers
Target: https://lovable.dev/signup (Lovable.dev registration page)
Turnstile sitekey: 0x4AAAAAAAHfngZVf0MDFkKR
Turnstile mode: auto (Cloudflare decides whether to challenge)
Widget action: signup-email-password
Verified blockers
1. CDP-level detection (primary blocker)
Cloudflare detects the Chrome DevTools Protocol connection itself. This is NOT removable via stealth patches:
// This does NOT bypass Turnstile detection
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
Evidence: Even when connecting to the user's REAL Chrome (not Playwright's Chromium) with navigator.webdriver=undefined, the Turnstile iframe shows auto_timeout in the URL path:
/auto/fbE/auto_timeout/normal?lang=auto
2. window.turnstile.execute() returns empty
// window.turnstile IS available with execute() method
window.turnstile.execute('cf-chl-widget-XXXXX');
// Returns success, but token remains empty
window.turnstile.getResponse('cf-chl-widget-XXXXX'); // → ''
Even the programmatic API refuses to return a token when CDP is connected.
3. 2captcha ERROR_CAPTCHA_UNSOLVABLE
2captcha Turnstile solving works by visiting the target URL in a worker's browser. Since Lovable's Turnstile is in auto_timeout for ALL automated browsers, the workers also see auto_timeout and cannot solve it.
2captcha returns: ERROR_CAPTCHA_UNSOLVABLE (after 15-20s attempt)
2captcha balance: $7.03 (sufficient, not the issue)
4. turnstile-signup endpoint accepts token but Firebase blocking function rejects
Lovable has a server endpoint /api/turnstile-signup that validates the Turnstile token with Cloudflare. Even when this endpoint returns 204 (token accepted), the Firebase Cloud Function (BLOCKING_FUNCTION_ERROR_RESPONSE) rejects the signup with:
error: BLOCKING_FUNCTION_ERROR_RESPONSE (internal-error)
The blocking function checks Turnstile token validity plus IP match. If the token was solved from a different IP (2captcha's IP), the function rejects even if the token itself is valid.
What was tested and failed
| Approach | Tools | Result |
|---|---|---|
| Headless Playwright | Playwright chromium | iframe empty, auto_timeout |
| Headed Playwright + stealth | --disable-blink-features=AutomationControlled |
iframe empty |
| Playwright + real Chrome | channel='chrome' + user data dir |
iframe renders, but auto_timeout |
| Playwright + real Chrome + init script | add_init_script overriding webdriver |
webdriver=undefined confirmed, but still auto_timeout |
| rebrowser_playwright | from rebrowser_playwright import async_playwright |
iframe empty |
| Headed natural mouse | page.mouse.move(steps=N) + page.mouse.click() |
checkbox visible, click → failure_retry |
| Chrome DevTools MCP | CDP Input.dispatchMouseEvent via DevTools tools |
checkbox visible, click → failure_retry |
| window.turnstile.execute() | programmatic execution | returns success, token empty |
| 2captcha | Turnstile solving service | ERROR_CAPTCHA_UNSOLVABLE |
| 2captcha + curl_cffi | solve + proxy | ERROR_CAPTCHA_UNSOLVABLE |
| pyautogui | physical click via Windows SendInput | NOT FULLY TESTED (window focus issue) |
What MIGHT work (untested)
- pyautogui — physical mouse/keyboard via Windows SendInput API. Chrome must be launched WITHOUT
--remote-debugging-port. Cloudflare sees real human input events and cannot distinguish them. - User clicking once — always works but defeats the purpose of automation.
- OAuth (Google/GitHub) — bypasses Turnstile entirely. GitHub OAuth opens popup window; GitHub login page may have its own anti-bot (DataDome CAPTCHA blocked CDP Chrome in tests).
Turnstile detection signals (probable)
Based on what failed and what partially worked:
- CDP WebSocket connection — Chrome DevTools Protocol uses a WebSocket connection. Cloudflare can detect the presence of this connection.
- navigator.webdriver — standard check (trivial to spoof, not the primary signal)
- Chrome automation flags —
--enable-automationadds a info bar and changesnavigator.webdriver. Playwright passes--disable-blink-features=AutomationControlledto suppress this. - Input event patterns — programmatic
Input.dispatchMouseEventhas different timing/coordinate patterns than human input window.chrome.runtimepresence/absence — automation tools often modify this
pyautogui approach (last resort)
See main SKILL.md section "pyautogui — coordinate-based GUI automation" for the full pattern.