Danh sách bài viết

Bài 112: CAPTCHA Bypass Cho Test Environment

CAPTCHA (reCAPTCHA, hCaptcha, Turnstile) chặn automation hoàn toàn — không thể solve bằng Playwright. Cách duy nhất hợp lệ là dev config chính app của mình để CAPTCHA tự động pass trong test environment. Bài này trình bày 4 strategy thực tế, test key từng provider, cách phối hợp với backend, và ranh giới đạo đức cần nắm rõ.

28/05/2026
0 lượt xem
1

Mục tiêu bài học

Sau bài này bạn sẽ:

  • Hiểu tại sao Playwright không thể solve CAPTCHA và approach hợp lệ duy nhất là gì.
  • Config test environment dùng test key của reCAPTCHA v2, v3, hCaptcha, Cloudflare Turnstile.
  • Phối hợp với backend để skip CAPTCHA verify qua header hoặc env flag.
  • Biết khi nào dùng bypass token và khi nào mock widget (và tại sao cần tránh mock).
  • Phân biệt rõ legitimate test bypass với illegal CAPTCHA defeat.
2

Vấn đề: CAPTCHA chặn automation

CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) được thiết kế đặc biệt để chặn automation. Playwright chạy trong browser thật, nhưng các provider như Google reCAPTCHA, hCaptcha, Cloudflare Turnstile vẫn phát hiện được headless/automation context và trả về failure.

Kịch bản thực tế:

  • Login form có reCAPTCHA v2 "I'm not a robot" → test click được checkbox nhưng challenge ẩn vẫn fail.
  • Signup form có hCaptcha → widget load, nhưng token submit về backend là invalid.
  • Rate-limited endpoint dùng Turnstile → Playwright request không có valid token → backend reject 403.

Playwright không cung cấp API nào để solve CAPTCHA, và đây là thiết kế có chủ đích. Cách tiếp cận duy nhất hợp lệ là dev config chính app của mình để CAPTCHA pass tự động trong test environment.

3

Ranh giới đạo đức — đọc trước khi làm

Bài này chỉ cover legitimate testing — tức là bạn là developer/owner của app đang test. Cần phân biệt rõ:

Tình huống Hợp lệ?
Bypass CAPTCHA trên app mình sở hữu trong test env (dev config) Hợp lệ
Bypass CAPTCHA trên app bên thứ ba (scraping, spam, bot) Vi phạm ToS, có thể illegal
Dùng CAPTCHA solver service (2Captcha, Anti-Captcha) cho site người khác Vi phạm ToS
OCR/AI model để defeat CAPTCHA production của bên thứ ba Vi phạm ToS, có thể illegal

Tất cả kỹ thuật trong bài này đều yêu cầu bạn có quyền config backend và frontend của app. Nếu bạn không sở hữu app đó, không có strategy nào ở đây áp dụng được.

4

Strategy 1: Test key từ provider

Các CAPTCHA provider cung cấp sẵn test key dành cho development/testing. Khi app dùng test key, CAPTCHA widget luôn pass mà không cần user interaction.

Đây là strategy được khuyến nghị nhất vì:

  • Chính thức từ provider — không vi phạm bất kỳ ToS nào.
  • Frontend vẫn render CAPTCHA widget bình thường (test gần với production hơn).
  • Backend verify token của test key cũng pass → end-to-end flow hoàn chỉnh.
  • Không cần sửa backend logic.

reCAPTCHA v2 test key

Google cung cấp test key chính thức:

# Site key (dùng trong frontend HTML)
RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI

# Secret key (dùng trong backend để verify token)
RECAPTCHA_SECRET_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe

Config app với test key này trong test environment. CAPTCHA widget sẽ render bình thường, nhưng khi submit — cả frontend validation lẫn backend server-side verify đều pass.

// playwright.config.ts — không cần config gì đặc biệt,
// chỉ cần app đang dùng test key ở test env
export default defineConfig({
  use: {
    baseURL: process.env.TEST_BASE_URL, // app chạy với RECAPTCHA_SITE_KEY=test key
  },
});

Playwright test bình thường, không cần xử lý CAPTCHA:

// login.spec.ts
test('login với CAPTCHA', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('secret');
  // reCAPTCHA widget đã auto-pass vì test key
  await page.getByRole('button', { name: 'Đăng nhập' }).click();
  await expect(page).toHaveURL('/dashboard');
});
5

Strategy 2: Backend flag skip verify

Backend skip bước verify CAPTCHA token khi nhận diện được request từ test environment. Có 2 cách phổ biến:

Cách 1: NODE_ENV check

// backend/auth.ts
async function handleLogin(req: Request) {
  const { email, password, captchaToken } = req.body;

  // Skip CAPTCHA verify trong test env
  if (process.env.NODE_ENV !== 'test') {
    await verifyCaptcha(captchaToken);
  }

  // Tiếp tục xác thực email/password
  const user = await authenticateUser(email, password);
  return createSession(user);
}

Rủi ro: Nếu build production vô tình set NODE_ENV=test, bypass sẽ active trên production.

Cách 2: Secret test header

An toàn hơn: backend chỉ skip verify khi nhận được header có secret chỉ test environment biết.

// backend/auth.ts
async function handleLogin(req: Request) {
  const { email, password, captchaToken } = req.body;

  const testHeader = req.headers['x-test-mode'];
  const isTestMode = testHeader === process.env.TEST_BYPASS_SECRET;

  if (!isTestMode) {
    await verifyCaptcha(captchaToken);
  }

  const user = await authenticateUser(email, password);
  return createSession(user);
}
// playwright.config.ts
export default defineConfig({
  use: {
    extraHTTPHeaders: {
      'x-test-mode': process.env.TEST_BYPASS_SECRET!,
    },
  },
});
# .env.test
TEST_BYPASS_SECRET=a9f3b2c1d4e5f6g7h8i9j0k1l2m3n4o5

Secret phải đủ dài (32+ ký tự random), không được commit vào repo, chỉ inject qua CI secrets. Nếu secret leak thì hacker có thể gửi request tới backend test env mà không cần CAPTCHA — nhưng miễn là không leak sang production env thì không ảnh hưởng người dùng thật.

Playwright sẽ tự động gửi header này trong mọi request, kể cả API call qua page.requestrequest fixture.

6

Strategy 3: Bypass token

Backend accept một "magic test token" đặc biệt thay vì token thật từ CAPTCHA provider. Playwright điền giá trị này vào field hidden CAPTCHA trước khi submit form.

// backend/captcha-verify.ts
export async function verifyCaptchaToken(token: string): Promise {
  // Magic test token — chỉ accept trong test env
  if (
    process.env.NODE_ENV !== 'production' &&
    token === process.env.CAPTCHA_TEST_BYPASS_TOKEN
  ) {
    return true;
  }

  // Verify thật với provider API
  const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
    method: 'POST',
    body: new URLSearchParams({
      secret: process.env.RECAPTCHA_SECRET_KEY!,
      response: token,
    }),
  });
  const data = await response.json();
  return data.success === true;
}

Playwright set token này trước khi submit:

// Cách 1: Nếu CAPTCHA token nằm trong hidden input
await page.locator('#g-recaptcha-response').evaluate(
  (el, token) => { (el as HTMLInputElement).value = token; },
  process.env.CAPTCHA_TEST_BYPASS_TOKEN
);

// Cách 2: Inject qua global variable mà form submit handler đọc
await page.evaluate((token) => {
  (window as any).captchaToken = token;
}, process.env.CAPTCHA_TEST_BYPASS_TOKEN);

await page.getByRole('button', { name: 'Submit' }).click();

Strategy này yêu cầu hiểu rõ cách frontend đọc CAPTCHA token trước khi submit. Với reCAPTCHA v2, token nằm trong #g-recaptcha-response. Với v3, token thường được set qua callback. Cần đọc source code frontend để biết chính xác.

7

Strategy 4: Mock CAPTCHA widget

Intercept request load CAPTCHA script và trả về mock response, hoặc mock hoàn toàn CAPTCHA API trong frontend.

// Mock reCAPTCHA script load
await page.route('https://www.google.com/recaptcha/**', (route) => {
  route.fulfill({
    status: 200,
    contentType: 'application/javascript',
    body: `
      // Mock grecaptcha global
      window.grecaptcha = {
        ready: (cb) => cb(),
        execute: () => Promise.resolve('mock-token-12345'),
        render: () => 0,
        getResponse: () => 'mock-token-12345',
        reset: () => {},
      };
    `,
  });
});
// Mock hCaptcha script
await page.route('https://js.hcaptcha.com/**', (route) => {
  route.fulfill({
    status: 200,
    contentType: 'application/javascript',
    body: `
      window.hcaptcha = {
        render: () => 'widget-id',
        execute: () => Promise.resolve({ response: 'mock-hcaptcha-token' }),
        getResponse: () => 'mock-hcaptcha-token',
        reset: () => {},
      };
    `,
  });
});

Strategy này cần kết hợp với backend accept mock token (strategy 2 hoặc 3), vì backend vẫn cần verify token.

Tại sao không nên dùng strategy này làm primary:

  • Fragile: provider thay đổi script URL hoặc API surface → mock break.
  • CAPTCHA widget không thật render → test không bao gồm CAPTCHA rendering behavior.
  • Phức tạp để maintain — mỗi provider có API khác nhau, và API thay đổi theo phiên bản.
  • Strategy 1 (test key) cover cả frontend lẫn backend một cách clean hơn nhiều.

Dùng strategy này chỉ khi provider không có test key và không thể sửa backend (trường hợp hiếm).

8

Test key của từng provider

Google reCAPTCHA v2

Google cung cấp test key chính thức trong tài liệu:

Site key:   6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
Secret key: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe

Khi dùng test key này: widget render với nhãn "This reCAPTCHA is for testing purposes only." và luôn pass. Backend verify với secret test key cũng trả về success: true.

Google reCAPTCHA v3

reCAPTCHA v3 không có UI widget — nó chạy ngầm và trả về score (0.0 đến 1.0). Test key của v2 không áp dụng cho v3. Thay vào đó:

  • Dùng strategy 2 (backend flag) để skip server-side score check khi test.
  • Hoặc dùng strategy 3: backend accept token đặc biệt với score mặc định 0.9.

Google chưa cung cấp test key public cho v3 tính đến v1 API hiện tại.

hCaptcha

hCaptcha cung cấp test sitekey và secret chính thức:

Site key:   10000000-ffff-ffff-ffff-000000000001
Secret key: 0x0000000000000000000000000000000000000001

Với test sitekey này, hCaptcha widget luôn pass và backend verify với test secret cũng trả về success. Tài liệu chính thức: docs.hcaptcha.com (section "Testing").

Cloudflare Turnstile

Cloudflare cung cấp 3 loại test sitekey với behavior khác nhau:

# Always passes (dùng cho test automation)
Site key: 1x00000000000000000000AA
Secret:   1x0000000000000000000000000000000AA

# Always blocks (test rejected flow)
Site key: 2x00000000000000000000AB
Secret:   2x0000000000000000000000000000000AA

# Forces interactive challenge
Site key: 3x00000000000000000000FF

Cho test automation dùng 1x00000000000000000000AA. Tài liệu: developers.cloudflare.com/turnstile/troubleshooting/testing/.

Tổng hợp

Provider Test key available? Behavior
reCAPTCHA v2 Có (chính thức) Luôn pass
reCAPTCHA v3 Không có public test key Dùng strategy 2/3
hCaptcha Có (chính thức) Luôn pass
Cloudflare Turnstile Có (chính thức, 3 loại) Always pass / block / interactive
9

Pattern config env var switch

Pattern chuẩn: dùng env var để switch giữa test key và production key. Không hard-code key trực tiếp trong code.

# .env.test (không commit secret key lên repo)
RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
RECAPTCHA_SECRET_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001
HCAPTCHA_SECRET_KEY=0x0000000000000000000000000000000000000001
TURNSTILE_SITE_KEY=1x00000000000000000000AA
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
# .env.production (quản lý qua CI secrets / secrets manager)
RECAPTCHA_SITE_KEY=6Lc...real_site_key...
RECAPTCHA_SECRET_KEY=6Lc...real_secret_key...
HCAPTCHA_SITE_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
HCAPTCHA_SECRET_KEY=0x...real_secret...

Frontend đọc site key qua env var:

// next.config.ts (ví dụ Next.js)
const nextConfig = {
  env: {
    NEXT_PUBLIC_RECAPTCHA_SITE_KEY: process.env.RECAPTCHA_SITE_KEY,
  },
};
// LoginForm.tsx
import ReCAPTCHA from 'react-google-recaptcha';

export function LoginForm() {
  return (
    
{/* ... form fields ... */} ); }

Với config này, khi deploy test environment với .env.test, CAPTCHA tự động dùng test key mà không cần thay đổi code. Production deploy dùng .env.production với key thật.

CI pipeline setup:

# .github/workflows/e2e.yml
- name: Run E2E tests
  env:
    RECAPTCHA_SITE_KEY: ${{ secrets.TEST_RECAPTCHA_SITE_KEY }}
    RECAPTCHA_SECRET_KEY: ${{ secrets.TEST_RECAPTCHA_SECRET_KEY }}
  run: npx playwright test

Dù test key là public knowledge, vẫn nên inject qua secrets để CI config nhất quán với secret management workflow.

10

Pitfall thường gặp

Pitfall 1: Dùng production key trong test environment

Test environment vô tình deploy với production CAPTCHA key. CAPTCHA sẽ chặn automation và mọi E2E test liên quan đến form có CAPTCHA đều fail. Playwright log thường hiện lỗi CAPTCHA verification failed hoặc form submit trả về 400/422.

Fix: Kiểm tra env var trong test setup:

// global-setup.ts
export default async function globalSetup() {
  const siteKey = process.env.RECAPTCHA_SITE_KEY;
  const testKey = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI';
  if (siteKey && siteKey !== testKey) {
    console.warn(
      '[CAPTCHA] RECAPTCHA_SITE_KEY không phải test key.' +
      ' E2E test có thể fail trên form có CAPTCHA.'
    );
  }
}

Pitfall 2: Backend test flag leak sang production

Backend check NODE_ENV !== 'production' nhưng staging/UAT environment set NODE_ENV=development → CAPTCHA bypass active trên staging. Hacker tìm được staging URL có thể submit form không cần CAPTCHA.

Fix: Dùng secret header thay vì NODE_ENV check. Secret header chỉ test có, staging và production không inject header này.

Pitfall 3: Mock CAPTCHA script break sau provider update

Mock dựa trên URL pattern https://www.google.com/recaptcha/**. Provider thêm domain phụ, đổi path, hoặc thay đổi API surface của global object → mock không còn hoạt động và form submit bị lỗi JavaScript.

Fix: Migrate về strategy 1 (test key) hoặc strategy 2 (backend flag). Tránh phụ thuộc vào URL pattern của provider third-party.

Pitfall 4: Nhầm legitimate test bypass với illegal defeat

Các kỹ thuật trong bài này chỉ hoạt động khi bạn control cả frontend lẫn backend của app. Nếu ai đó thử dùng strategy 2 (backend flag) cho site người khác → không có tác dụng vì bạn không control được backend đó.

Cần phân biệt: bypass CAPTCHA của app mình bằng dev config ≠ defeat CAPTCHA của site người khác. Hai việc này khác nhau hoàn toàn về kỹ thuật, pháp lý, và đạo đức.

Pitfall 5: Test token hard-coded trong source code

Developer hard-code CAPTCHA_TEST_BYPASS_TOKEN=abc123 trực tiếp trong backend code. Token này commit lên repo, hacker tìm thấy → có thể bypass CAPTCHA trên test environment (hoặc tệ hơn nếu code đó deploy sai nơi).

Fix: Luôn đọc bypass token từ env var, không hard-code. Rotate token định kỳ.

11

Limitation

Cần nhận thức rõ những gì các strategy này không test được:

  • CAPTCHA rendering behavior: Strategy 1 (test key) render widget thật, nhưng strategy 2-4 thì không. Nếu cần test widget render đúng, dùng visual snapshot riêng với manual review.
  • Bot detection accuracy: Test environment không thể verify rằng CAPTCHA production thực sự chặn được bot — đây là test riêng của provider, không phải của E2E suite.
  • Backend CAPTCHA verify logic: Khi dùng strategy 2 (skip verify), backend code path verify CAPTCHA hoàn toàn không được test. Nếu có bug trong verifyCaptcha(), E2E test sẽ không phát hiện được.
  • Provider downtime: Strategy 1 vẫn gọi provider CDN để load widget. Nếu Google/hCaptcha CDN không available trong CI environment (network restriction), widget không load được dù là test key. Strategy 2 không có vấn đề này.
  • Yêu cầu phối hợp với backend team: Strategies 1-3 đều cần backend team setup tương ứng. Playwright test không thể đơn phương làm được gì nếu backend không có test mode.
12

Quiz

Câu 1. reCAPTCHA v2 test site key là 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI. Khi frontend dùng test key này mà backend vẫn dùng production secret key để verify, kết quả sẽ là:

  • A. Verification vẫn pass vì test key luôn pass cả frontend lẫn backend
  • B. Verification fail vì test site key phải đi kèm test secret key
  • C. Widget không render
  • D. Verification pass một phần — frontend pass nhưng backend timeout

Đáp ánB. Test site key sinh ra token có thể verify được, nhưng chỉ khi dùng test secret key tương ứng (6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe). Nếu backend dùng production secret key để verify token từ test site key, Google trả về error-codes: ["invalid-input-secret"] hoặc success: false.

Câu 2. Strategy "backend secret header" an toàn hơn "NODE_ENV check" vì lý do nào?

  • A. Header dễ gửi hơn từ Playwright
  • B. NODE_ENV có thể bị set nhầm trên staging/UAT khiến bypass vô tình active
  • C. NODE_ENV không phải env var hợp lệ trong Node.js
  • D. Secret header tự động rotate mỗi ngày

Đáp ánB. NODE_ENV thường được set là development hoặc staging trên nhiều non-production env. Nếu code check NODE_ENV !== 'production', bypass CAPTCHA sẽ active trên tất cả các env đó, không chỉ test. Secret header chỉ được inject từ Playwright test config, không bao giờ được gửi từ browser thật.

Câu 3. Cloudflare Turnstile sitekey 2x00000000000000000000AB được dùng để làm gì trong test?

  • A. Test flow khi CAPTCHA luôn pass
  • B. Test flow khi CAPTCHA luôn bị block (verify fail)
  • C. Test interactive challenge
  • D. Đây là production key mặc định của Turnstile

Đáp ánB. Turnstile cung cấp 3 loại test key: 1x00000000000000000000AA luôn pass, 2x00000000000000000000AB luôn block (test rejected flow), và 3x00000000000000000000FF force interactive challenge.

Câu 4. Bạn muốn test form signup có reCAPTCHA v3. Provider chưa cung cấp test key cho v3. Approach nào phù hợp nhất?

  • A. Dùng test key của reCAPTCHA v2 cho v3
  • B. Dùng strategy 2: backend skip score check khi nhận secret header từ test
  • C. Dùng CAPTCHA solver service để lấy token thật
  • D. Bỏ qua test form signup vì không giải được

Đáp ánB. reCAPTCHA v2 test key không áp dụng cho v3 (khác API). CAPTCHA solver service cho site của mình là không cần thiết và thêm dependency phức tạp. Strategy 2 (backend flag) cho phép E2E test pass mà không cần token hợp lệ, phù hợp khi provider không có test key.

Câu 5. Mock CAPTCHA script bằng page.route() là strategy ít được khuyến nghị nhất vì:

  • A. page.route() không hỗ trợ intercept external domain
  • B. Mock phụ thuộc vào URL pattern và API surface của provider, dễ break khi provider update
  • C. Strategy này vi phạm ToS của CAPTCHA provider
  • D. Chỉ hoạt động với Chromium, không hoạt động với Firefox và WebKit

Đáp ánB. page.route() hỗ trợ intercept bất kỳ URL nào kể cả external domain. Vấn đề là mock phụ thuộc vào URL pattern chính xác và API object (window.grecaptcha, window.hcaptcha) đang được mock. Khi provider thay đổi CDN path, script URL, hoặc API surface, mock sẽ break. Strategy 1 (test key) không có vấn đề này.

13

Bài tiếp theo

Bài 113: IndexedDB Storage State — lưu và restore IndexedDB data như một phần của storage state để test các app dùng client-side database (offline-first PWA, PouchDB, Dexie.js).