> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# بديل reCAPTCHA يحترم الخصوصية مع ALTCHA
- URL: https://arabroot.io/articles/بديل-recaptcha-يحترم-الخصوصية-مع-altcha/
- Published: 2026-10-13T10:00:00.000Z
- Updated: 2026-10-13T13:26:42.000Z
- Description: كيف تحمي نموذج التسجيل من الروبوتات بإثبات العمل على سيرفرك، دون أن ترسل بيانات زوارك إلى طرف ثالث.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الشبكات والبنية التحتية, الشبكات والوصول, الاستضافة الذاتية

إذا فتحت نموذج تسجيل أو نموذج حجز للعموم، فسوف يصل إليه بعد أيام برنامج يملؤه آلاف المرات بحسابات وهمية ورسائل دعائية، وقد رأينا ذلك في دليل [Easy!Appointments](https://arabroot.io/articles/easy-appointments-%D8%AD%D8%AC%D8%B2-%D9%85%D9%88%D8%A7%D8%B9%D9%8A%D8%AF-%D8%A7%D9%84%D8%B9%D9%8A%D8%A7%D8%AF%D8%A7%D8%AA-%D9%88%D9%85%D8%B1%D8%A7%D9%83%D8%B2-%D8%A7%D9%84%D8%AE%D8%AF%D9%85%D8%A9/) عندما حجز سكربت بسيط عشرة مواعيد في ثانية. والحل المعتاد أن تضع في النموذج اختباراً من نوع CAPTCHA، وأشهرها reCAPTCHA من Google وTurnstile من Cloudflare، وكلاهما يعمل جيداً، ولكن كلاهما يعني أن متصفح كل زائر يحمل سكربتاً من شركة أخرى ويرسل إليها بيانات عن الزيارة.

وفي هذا المقال سوف نحمي نموذج تسجيل بأداة ALTCHA التي تعمل كلها على سيرفرك، ونتحقق من الحل على السيرفر بأنفسنا، ثم نهاجم النموذج بإعادة إرسال نفس الحل Replay Attack لنرى لماذا لا يكفي التحقق من التوقيع وحده. وسوف نناقش ما يلي:

- ما الذي يرسله reCAPTCHA وTurnstile إلى الشركتين، من وثائقهما الرسمية.
- فكرة إثبات العمل Proof of Work في ALTCHA، والرخصة، وحالة مشروع mCaptcha.
- تجربة عملية: نموذج تسجيل عربي مع ALTCHA، والتحقق على السيرفر بمكتبة altcha-lib.
- نكسرها: إعادة إرسال نفس الحل، والحل المعدل، ونموذج دون حل، ثم الإصلاح.
- سهولة الوصول Accessibility: لوحة المفاتيح وقارئ الشاشة وفحص axe.

## ما الذي يعرفه الطرف الثالث عن زوارك؟

عندما تضع reCAPTCHA أو Turnstile في صفحتك، يحمل المتصفح سكربت الشركة من نطاقاتها، ويتحدث معها مباشرة، ثم يرسل سيرفرك التوكن إلى الشركة لتقول له هل الزائر إنسان. والجدول التالي يبين ما تقوله وثائق كل شركة عند كتابة المقال:

| الخدمة         | ما تذكره الوثائق الرسمية                                                                                                                                                                                                                                               | السعر                                                                                                                                                                                                                                                            |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| خدمة reCAPTCHA | تضع Cookie باسم \_GRECAPTCHA لتحليل الخطر، ويسمح لك بإخفاء الشارة Badge بشرط أن تكتب في الصفحة أنها محمية ب reCAPTCHA، بحسب [الأسئلة الشائعة](https://developers.google.com/recaptcha/docs/faq?ref=arabroot.io)                                                        | الخطة Essentials مجانية حتى 10,000 تقييم في الشهر لكل منظمة، بحسب [صفحة الخطط](https://docs.cloud.google.com/recaptcha/docs/compare-tiers?ref=arabroot.io) المحدثة في 9 أكتوبر 2026                                                                              |
| خدمة Turnstile | تعالج عنوان IP وبصمة TLS وترويسة User-Agent والموقع الذي فيه ال Widget، وتقول الشركة إن الغرض اكتشاف الروبوتات فقط وليس تحديد الأشخاص أو استهدافهم، بحسب [ملحق الخصوصية](https://www.cloudflare.com/turnstile-privacy-policy/?ref=arabroot.io) المحدث في 18 يونيو 2025 | خطة مجانية شرحنا حدودها في [خدمات Cloudflare](https://arabroot.io/articles/%D8%AE%D8%AF%D9%85%D8%A7%D8%AA-cloudflare-%D9%88%D8%A8%D8%AF%D8%A7%D8%A6%D9%84%D9%87%D8%A7-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1/#bots-turnstile) |

وقد يتساءل البعض: إذا كانت الشركتان تقولان إن البيانات لاكتشاف الروبوتات فقط، فأين المشكلة؟ والإجابة أن المشكلة ليست في نية الشركة، وإنما في أن عنوان كل زائر لنموذجك يصل إلى طرف آخر لم يختره الزائر، وهذا يحتاج أن تذكره في سياسة الخصوصية، وقد يكون غير مقبول في أنظمة حكومية أو صحية تشترط ألا تخرج بيانات المستخدمين إلى خدمات خارجية، وهو أيضاً اعتماد على خدمة خارجية قد تتغير شروطها أو أسعارها، فالحد المجاني في reCAPTCHA مثلاً محسوب لكل منظمة على كل حساباتها ومواقعها كما تقول صفحة الخطط.

## الفكرة: اجعل الإرسال يكلف شيئاً

بدل أن تسأل الزائر عن إشارات المرور في الصور، تطلب من متصفحه أن يحل مسألة حسابية تأخذ جزءاً من الثانية، وهذا ما يسمى إثبات العمل Proof of Work، وهي نفس فكرة [Anubis](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D9%85%D9%88%D9%82%D8%B9%D9%83-%D9%85%D9%86-%D8%B1%D9%88%D8%A8%D9%88%D8%AA%D8%A7%D8%AA-%D8%A7%D9%84%D8%B0%D9%83%D8%A7%D8%A1-%D8%A7%D9%84%D8%A7%D8%B5%D8%B7%D9%86%D8%A7%D8%B9%D9%8A-%D9%85%D8%B9-anubis/)، والفرق أن Anubis يقف أمام الموقع كله، بينما ALTCHA يحمي نموذجاً واحداً. والزائر الحقيقي يرسل النموذج مرة، فلا يكاد يشعر بالمسألة، أما البرنامج الذي يريد إرسال عشرة آلاف نموذج فعليه أن يحل عشرة آلاف مسألة.

الصورة التالية تبين ما يحدث بين المتصفح وسيرفرك:

![المتصفح يطلب مسألة من سيرفرك، فيرد السيرفر بمسألة موقعة بمفتاح HMAC، فيحلها المتصفح بخوارزمية PBKDF2 في Web Workers، ثم يرسل النموذج مع الحل، فيتحقق السيرفر من التوقيع والصلاحية والحل وأن الحل لم يستخدم من قبل](https://arabroot.io/content/images/2026/10/altcha-mcaptcha-01-flow.webp)

كل الخطوات بين المتصفح وسيرفرك، والتحقق الأخير من عدم تكرار الحل مسؤوليتك

ولاحظ أن ALTCHA لا يثبت أن الزائر إنسان، وإنما يجعل كل إرسال يكلف وقتاً من المعالج، فالبرنامج الذي يشغل متصفحاً حقيقياً سوف يحل المسألة كأي زائر، والفائدة أنه لن يستطيع إرسال آلاف النماذج في دقيقة. لذلك اجمعه مع حد معدل الطلبات كما في [Rate Limiting وخوارزمياته](https://arabroot.io/articles/%D8%AA%D8%AD%D8%AF%D9%8A%D8%AF-%D9%85%D8%B9%D8%AF%D9%84-%D8%A7%D9%84%D8%B7%D9%84%D8%A8%D8%A7%D8%AA-rate-limiting-%D9%88%D8%AE%D9%88%D8%A7%D8%B1%D8%B2%D9%85%D9%8A%D8%A7%D8%AA%D9%87/)، ولا تعتبره بديلاً عنه.

## الأداتان: ALTCHA وmCaptcha

| البند                | أداة ALTCHA                                              | أداة mCaptcha                                        |
| -------------------- | -------------------------------------------------------- | ---------------------------------------------------- |
| الرخصة               | MIT للواجهة والمكتبات                                    | AGPL-3.0                                             |
| آخر إصدار            | الواجهة 3.3.0 والمكتبة altcha-lib 2.6.0 في 5 أكتوبر 2026 | الإصدار 0.1.0 في 15 مارس 2024، وهو الإصدار الوحيد    |
| آخر نشاط في المستودع | أكتوبر 2026                                              | 7 أكتوبر 2025                                        |
| طريقة العمل          | واجهة Widget في الصفحة، ومكتبة تتحقق في كودك بلغات كثيرة | سيرفر مستقل بقاعدة بيانات تستضيفه، وWidget يتحدث معه |
| اللغة العربية        | ترجمة عربية ضمن الحزمة                                   | لم نجربها                                            |

### أداة ALTCHA

[ALTCHA](https://github.com/altcha-org/altcha?ref=arabroot.io) مشروع من Daniel Regeci وشركة BAU Software، وفيه جزءان: Web Component اسمه `altcha-widget` تضعه في النموذج، ومكتبة على السيرفر تنشئ المسألة وتتحقق من الحل، وأشهرها مكتبة [altcha-lib](https://github.com/altcha-org/altcha-lib?ref=arabroot.io) للغة TypeScript، ولها نظائر رسمية بلغات Go وPython وPHP وJava وغيرها. والإصدار الثالث يستخدم خوارزميات اشتقاق مفاتيح مثل PBKDF2 وScrypt وArgon2، والسبب أن هذه الخوارزميات أبطأ على المعالجات المتخصصة من SHA-256 وحده.

- **نقاط القوة:** لا يوجد طرف ثالث إطلاقاً، ولا توجد Cookies من الواجهة بحسب المشروع، والمكتبة صغيرة ولها نسخ بلغات كثيرة، والترجمة العربية موجودة.
- **نقاط الضعف:** لا يعرف شيئاً عن سمعة العناوين كما تعرف Cloudflare وGoogle، والحماية من إعادة الإرسال مسؤوليتك كما سيأتي، وتحتاج الصفحة إلى HTTPS، والمسألة البديلة بالصورة والصوت تحتاج خدمة Sentinel من نفس الشركة أو تنفيذاً خاصاً بك.

### أداة mCaptcha

[mCaptcha](https://github.com/mCaptcha/mCaptcha?ref=arabroot.io) مكتوب بلغة Rust، ويعمل كسيرفر مستقل تستضيفه مع قاعدة بيانات، ويزيد صعوبة المسألة تلقائياً عندما يرتفع عدد الطلبات على الموقع. وفي ملف README ما زالت عبارة «STATUS: ACTIVE DEVELOPMENT»، ولكن الإصدار الوحيد المنشور هو 0.1.0 في مارس 2024، وآخر Commit في المستودع كان في 7 أكتوبر 2025، أي قبل سنة من كتابة المقال، وفي المستودع 54 مسألة مفتوحة.

- **نقاط القوة:** رفع الصعوبة تلقائياً مع الضغط، ورخصة AGPL-3.0 تضمن بقاء التعديلات مفتوحة.
- **نقاط الضعف:** مشروع لم يصدر منه غير إصدار واحد ولم يتغير منذ سنة، وسيرفر إضافي بقاعدة بيانات تحتاج أن تراقبه وتحدثه.

لذلك لم نبن عليه تجربة، ولا ننصح به لموقع مهم الآن، والأنسب أن تراجع حالته قبل أن تعتمد عليه.

## التجربة: نموذج تسجيل عربي مع ALTCHA

الترتيب في التجربة كما يلي:

| المكون           | الإصدار                                | دوره                                                                |
| ---------------- | -------------------------------------- | ------------------------------------------------------------------- |
| السيرفر          | node:24-alpine مع Node.js 24.21.0      | يعرض النموذج، وينشئ المسائل، ويتحقق من الحل بمكتبة altcha-lib 2.6.0 |
| الواجهة          | الحزمة altcha 3.3.0                    | ملف altcha.js وملف الترجمة العربية                                  |
| ال Reverse Proxy | nginx:1.31.6-alpine                    | شهادة ذاتية التوقيع للمختبر                                         |
| المتصفح          | أداة Playwright 1.64.0 مع Chromium 156 | يملأ النموذج بلوحة المفاتيح ويرسله                                  |

وثبتنا الحزمتين داخل Container حتى لا نشغل npm على الجهاز نفسه:

```bash
docker run --rm --name lab-edge1-npm -v "$PWD:/app" -w /app node:24-alpine \
  npm i --no-audit --no-fund altcha@3.3.0 altcha-lib@2.6.0
```

### النموذج

النموذج صفحة HTML عادية، وكل ما يضيفه ALTCHA سطران لتحميل الواجهة والترجمة، ووسم `altcha-widget` داخل النموذج:

```html
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<script type="module" src="/altcha.js"></script>
<script type="module" src="/altcha-ar.js"></script>
</head>
<body>
  <form id="signup" method="post" action="/signup">
    <label for="name">الاسم</label>
    <input id="name" name="name" type="text" autocomplete="name" required>
    <label for="email">البريد الإلكتروني</label>
    <input id="email" name="email" type="email" autocomplete="email" required>
    <label for="password">كلمة المرور</label>
    <input id="password" name="password" type="password" autocomplete="new-password" required>
    <altcha-widget challenge="/altcha/challenge" language="ar"></altcha-widget>
    <button type="submit">تسجيل</button>
  </form>
</body>
</html>
```

في الإعداد أعلاه لاحظ التالي:

- الملفان يقدمان من سيرفرك نفسه، فلا يحمل المتصفح أي شيء من نطاق آخر.
- الخاصية `challenge` هي الرابط الذي تطلب منه الواجهة المسألة، والخاصية `language="ar"` تحتاج ملف الترجمة العربية.
- عندما يحل المتصفح المسألة تضع الواجهة الحل في حقل مخفي اسمه `altcha`، فيرسل مع بقية النموذج.

### التحقق على السيرفر

السيرفر يحتاج مسارين: مسار ينشئ المسألة ويوقعها بمفتاح سري، ومسار التسجيل الذي يتحقق من الحل قبل أن ينشئ الحساب. وهذا الجزء المهم من الملف `server.mjs`:

```javascript
import { createChallenge, verifySolution, randomInt } from 'altcha-lib';
import { deriveKey } from 'altcha-lib/algorithms/pbkdf2';

const SECRET = process.env.ALTCHA_HMAC_SECRET;            // long random value from the environment, never in code
const KEY_SECRET = process.env.ALTCHA_HMAC_KEY_SECRET;
const TTL = +(process.env.CHALLENGE_TTL_SECONDS || 600);
const USE_STORE = process.env.REPLAY_STORE !== 'off';
const [CMIN, CMAX] = (process.env.COUNTER_RANGE || '5000-10000').split('-').map(Number);   // work per challenge
const used = new Map();                                   // challenge nonce -> expiry (ms); use Redis with several servers

function markUsed(id, expiresMs) {                        // true only for the first submission of this challenge
  const now = Date.now();
  for (const [k, exp] of used) if (exp < now) used.delete(k);
  if (used.has(id)) return false;
  used.set(id, expiresMs);
  return true;
}

// GET /altcha/challenge
const challenge = await createChallenge({
  algorithm: 'PBKDF2/SHA-256',
  cost: 5_000,
  counter: randomInt(CMIN, CMAX),
  deriveKey,
  expiresAt: new Date(Date.now() + TTL * 1000),
  hmacSignatureSecret: SECRET,
  hmacKeySignatureSecret: KEY_SECRET,
});

// POST /signup
const payload = JSON.parse(Buffer.from(form.get('altcha'), 'base64').toString('utf8'));
const result = await verifySolution({
  challenge: payload.challenge, solution: payload.solution, deriveKey,
  hmacSignatureSecret: SECRET, hmacKeySignatureSecret: KEY_SECRET,
});
if (!result.verified) return json(res, 403, { ok: false, reason: 'not verified', expired: result.expired, invalidSignature: result.invalidSignature, invalidSolution: result.invalidSolution });
if (USE_STORE) {
  const id = payload.challenge.parameters.nonce;
  const exp = (payload.challenge.parameters.expiresAt || 0) * 1000 || Date.now() + TTL * 1000;
  if (!markUsed(id, exp)) return json(res, 409, { ok: false, reason: 'payload already used' });
}
return json(res, 201, { ok: true, email: form.get('email'), verifyMs: Math.round(result.time * 10) / 10 });
```

في الكود أعلاه لاحظ التالي:

- المسألة موقعة بالمفتاح `ALTCHA_HMAC_SECRET`، فلا يستطيع المتصفح أن يخفف صعوبتها أو يطيل صلاحيتها، لأن أي تغيير يفسد التوقيع. والمفتاح قيمة عشوائية طويلة من متغيرات البيئة، ولا تكتبه في الكود.
- القيمة `counter` تحدد كم محاولة يحتاج المتصفح ليجد الحل، والقيم 5000 إلى 10000 هي قيم المثال في وثائق المكتبة، وقد جعلناها قابلة للتغيير لسبب سوف تراه في القياس.
- الدالة `verifySolution` تتحقق من التوقيع والصلاحية والحل، ولكنها لا تعرف هل استخدم هذا الحل من قبل، وهذا ما تفعله الدالة `markUsed` بجدول للمسائل المستخدمة.
- الجدول هنا في ذاكرة العملية، وهذا يكفي لسيرفر واحد، أما مع عدة سيرفرات فالمكان الصحيح هو Redis بأمر `SET` مع الخيار `NX` ومدة صلاحية، كما تشرح [وثائق المكتبة](https://github.com/altcha-org/altcha-lib/blob/main/docs/store.md?ref=arabroot.io)، والسبب أن المهاجم سوف يرسل نفس الحل إلى كل السيرفرات.

### التسجيل بلوحة المفاتيح

كتبنا سكربت Playwright يفتح النموذج، ويفحصه بأداة axe، ثم يملأ الحقول وينتقل إلى مربع ALTCHA بالمفتاح Tab ويضغط المسافة، كما يفعل من لا يستخدم الفأرة، ثم يرسل النموذج ويحفظ الحل الذي أرسل حتى نستخدمه في الهجوم. والمخرج سوف يكون كما يلي:

```text
axe violations: none
focus after Tab: <input type="checkbox" id="altcha-checkbox-80383e386a" name="" required="">
widget label: أنا لست روبوتاً
verified after ms: 25622 | widget text: تم التحقق محمي بواسطة ALTCHA
POST /signup -> 201 {"ok":true,"email":"sara@example.com","verifyMs":45}
payload keys: challenge,solution | solution: {"counter":1504,"derivedKey":"8effe41ec167aba66cc990cc6dd3ea3f989e4e3ab98c4fb37b951c4cd3c8d55f","time":11926.2}
```

في المخرج أعلاه لاحظ التالي:

- فحص axe لم يجد أي مخالفة في الصفحة مع الواجهة، والانتقال بالمفتاح Tab وصل إلى مربع ALTCHA مباشرة بعد حقل كلمة المرور، والمسافة بدأت التحقق، أي أن النموذج كله يعمل بلوحة المفاتيح.
- عنوان المربع «أنا لست روبوتاً» بالعربية من ملف الترجمة، وبعد الحل أصبح «تم التحقق».
- السيرفر رد بالرمز 201، وكان التحقق من الحل على السيرفر سريعاً، 45 ملي ثانية في هذا التشغيل، لأن المكتبة في هذا الوضع تتحقق من توقيع المفتاح المشتق بدل أن تعيد الحساب كله.
- الحل في الحقل `altcha` هو المسألة نفسها مع الرقم `counter` والمفتاح المشتق `derivedKey`، مرمزة ب Base64.

أما الوقت فيحتاج قراءة صريحة: احتاج المتصفح هنا قرابة 26 ثانية حتى ظهر «تم التحقق»، والسبب أن الجهاز كان مشتركاً مع تجارب أخرى تستهلك المعالج بقوة وقت القياس، ولذلك خففنا المسألة في التجربة إلى `COUNTER_RANGE=1000-2000` بدل قيم الوثائق 5000 إلى 10000\. وللمقارنة حلت المكتبة نفسها في Node.js مسألة بالرقم 7500 والكلفة 5000 على نفس الجهاز، دون Web Workers أي على نواة واحدة، والمخرج كما يلي:

```text
counter 7500 solved {"counter":7500,"derivedKey":"b4ff333f453bb8990ed4e2c90e09eb0a1c2cd35fd87ec964be915f10669d6c7d","time":46310.4} ms 46288
```

أي 46 ثانية على نواة واحدة لجهاز مزدحم. وعلى حاسوب عادي غير مشغول، مع عدة Workers في المتصفح، يكون الوقت أقل بكثير، ولكن الدرس العملي أن تختار القيمتين `cost` و`counter` بعد أن تجرب النموذج على أضعف جهاز يستخدمه زوارك، وليس على حاسوبك.

والصورة التالية من نفس التشغيل بعد الحل:

![نموذج إنشاء حساب بالعربية فيه الاسم والبريد وكلمة المرور، وتحتها مربع ALTCHA مكتوب فيه تم التحقق مع علامة خضراء](https://arabroot.io/content/images/2026/10/altcha-mcaptcha-02-verified.webp)

نموذج التسجيل بعد التحقق، والنص العربي من ملف الترجمة في الحزمة

### نكسرها: إعادة إرسال نفس الحل

المهاجم الذكي لا يحل المسألة لكل حساب، وإنما يحلها مرة واحدة في متصفح، ثم يأخذ قيمة الحقل `altcha` ويرسلها مع ألف نموذج آخر. ولنرى ذلك شغلنا السيرفر أولاً دون جدول المسائل المستخدمة بالمتغير `REPLAY_STORE=off`، وسجلنا حساباً من المتصفح، ثم أرسلنا نفس الحل من `curl` بسكربت `replay.sh`:

```bash
P=$(cat out/altcha-payload.txt)
post() { curl -sk -w '  -> HTTP %{http_code}\n' "$U" --data-urlencode "name=$1" --data-urlencode "email=$2" --data-urlencode "password=x-lab-pass-1" ${3:+--data-urlencode "altcha=$3"}; }
echo "== replay 1 (same payload, another email)"; post "bot" "bot1@example.com" "$P"
echo "== replay 2"; post "bot" "bot2@example.com" "$P"
T=$(echo "$P" | base64 -d | sed 's/"counter":\([0-9]*\)/"counter":1/' | base64 -w0)
echo "== tampered counter"; post "bot" "bot3@example.com" "$T"
K=$(echo "$P" | base64 -d | sed 's/"derivedKey":"./"derivedKey":"0/' | base64 -w0)
echo "== tampered derivedKey"; post "bot" "bot5@example.com" "$K"
echo "== no altcha field"; post "bot" "bot4@example.com" ""
```

والمخرج دون الجدول سوف يكون كما يلي:

```text
== replay 1 (same payload, another email)
{"ok":true,"email":"bot1@example.com","verifyMs":1}  -> HTTP 201
== replay 2
{"ok":true,"email":"bot2@example.com","verifyMs":1.3}  -> HTTP 201
== tampered counter
{"ok":true,"email":"bot3@example.com","verifyMs":1.4}  -> HTTP 201
== tampered derivedKey
{"ok":false,"reason":"not verified","expired":false,"invalidSignature":false,"invalidSolution":true}  -> HTTP 403
== no altcha field
{"ok":false,"reason":"missing altcha field"}  -> HTTP 400
```

في المخرج أعلاه لاحظ التالي:

- نفس الحل أنشأ حسابين جديدين ببريدين مختلفين، وكان يمكن أن ينشئ ألفاً، فالتوقيع صحيح والحل صحيح والمسألة لم تنته صلاحيتها، والدالة `verifySolution` لا تعرف أنه استخدم من قبل.
- تغيير الرقم `counter` لم يغير شيئاً، والسبب أن المكتبة في هذا الوضع لا تعيد الحساب من الرقم، وإنما تتحقق من أن المفتاح المشتق `derivedKey` موقع بالمفتاح السري، فهو الدليل على العمل.
- تغيير حرف واحد في `derivedKey` رفض بالرمز 403 والسبب `invalidSolution`، والطلب دون الحقل رفض بالرمز 400.

ثم أعدنا تشغيل السيرفر بالجدول، وكررنا نفس الخطوات: تسجيل من المتصفح ثم نفس السكربت، والمخرج سوف يكون كما يلي:

```text
POST /signup -> 201 {"ok":true,"email":"sara@example.com","verifyMs":714.2}
== replay 1 (same payload, another email)
{"ok":false,"reason":"payload already used"}  -> HTTP 409
== replay 2
{"ok":false,"reason":"payload already used"}  -> HTTP 409
== tampered counter
{"ok":false,"reason":"payload already used"}  -> HTTP 409
== no altcha field
{"ok":false,"reason":"missing altcha field"}  -> HTTP 400
```

في المخرج أعلاه لاحظ التالي:

- التسجيل الأول من المتصفح نجح، ثم رفض كل إرسال آخر لنفس الحل بالرمز 409 والسبب `payload already used`، حتى مع بريد مختلف.
- في هذا التشغيل لم يكن سطر `derivedKey` قد أضيف إلى السكربت بعد، لذلك لا يظهر في المخرج، ونتيجته في التشغيل السابق.
- الجدول يحتفظ بكل مسألة حتى تنتهي صلاحيتها فقط، لأن الحل بعد ذلك يرفض على أي حال، وهذا يجعل حجمه محدوداً.

### والحل القديم؟

ثم شغلنا السيرفر بصلاحية قصيرة جداً، 20 ثانية بالمتغير `CHALLENGE_TTL_SECONDS=20`، والمخرج سوف يكون كما يلي:

```text
verified after ms: 23524
POST /signup -> 403 {"ok":false,"reason":"not verified","expired":true,"invalidSignature":null,"invalidSolution":null}
```

لاحظ أن الزائر نفسه هو الذي رفض هنا، لأن جهازه احتاج 23.5 ثانية ليحل المسألة، فانتهت صلاحيتها قبل أن يرسل النموذج، ثم رفضت كل محاولات إعادة الإرسال بعد 30 ثانية بنفس السبب `expired`. والصلاحية المناسبة تكفي لأبطأ زائر حتى يكتب النموذج ويرسله، والوثائق تستخدم 10 دقائق في أمثلتها، أما حماية إعادة الإرسال داخل هذه المدة فهي عمل جدول المسائل المستخدمة.

### إزالة التجربة

```bash
docker rm -f lab-edge1-altcha lab-edge1-front lab-edge1-pw
docker network rm lab-edge1-net
```

## سهولة الوصول

اختبار CAPTCHA بالصور من أكثر ما يشتكي منه مستخدمو قارئ الشاشة Screen Reader وكبار السن، ولهذا يهمنا هنا ما يحدث فعلاً، وفيما يلي ما رأيناه في التجربة وما تقوله المشاريع:

- **ALTCHA:** في تجربتنا لم يجد فحص axe 4.13.0 أي مخالفة في النموذج مع الواجهة، ووصلنا إلى المربع وشغلنا التحقق بلوحة المفاتيح وحدها، والنص عربي من ملف الترجمة. ويقول المشروع إن الواجهة متوافقة مع WCAG 2.2 بالمستوى AA، وإن المسألة البديلة بالصورة لها نسخة صوتية، ولم نختبر قارئ الشاشة بأنفسنا.
- **Turnstile:** تقول [وثائق Cloudflare](https://developers.cloudflare.com/turnstile/?ref=arabroot.io) إن Turnstile متوافق مع WCAG 2.2 بالمستوى AA، ولم نختبره بأنفسنا.
- **reCAPTCHA:** لم نختبره، فراجع سلوكه مع قارئ الشاشة على صفحتك قبل أن تعتمده.
- **mCaptcha:** لم نختبره لأننا لم نبن عليه تجربة.

ولا تكتف بما تقوله المشاريع عن نفسها، فالفحص الآلي يكشف جزءاً من المشكلات فقط، وقد شرحنا ما لا تراه الأدوات في [اختبار سهولة الوصول لموقع عربي](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%B3%D9%87%D9%88%D9%84%D8%A9-%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-accessibility-%D9%84%D9%85%D9%88%D9%82%D8%B9-%D8%B9%D8%B1%D8%A8%D9%8A/#manual)، والمبادئ في [سهولة الوصول Accessibility](https://arabroot.io/articles/%D8%B3%D9%87%D9%88%D9%84%D8%A9-%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-accessibility-%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%85%D9%88%D9%82%D8%B9%D8%A7-%D9%8A%D8%B3%D8%AA%D8%AE%D8%AF%D9%85%D9%87-%D8%A7%D9%84%D8%AC%D9%85%D9%8A%D8%B9/).

## أي حل تختار؟

| حالتك                                    | الأنسب                                   | والسبب                                       |
| ---------------------------------------- | ---------------------------------------- | -------------------------------------------- |
| لا تريد أن تخرج بيانات الزوار إلى أي طرف | أداة ALTCHA مع جدول المسائل المستخدمة    | كل شيء على سيرفرك                            |
| موقعك على Cloudflare أصلاً وتقبل سياستها | خدمة Turnstile                           | تعرف سمعة العناوين من ملايين المواقع         |
| تستخدم Google Cloud ولديك خبرة فيه       | خدمة reCAPTCHA                           | درجة خطر لكل طلب، مع حد مجاني شهري           |
| هجوم مستمر بحجم كبير على النموذج         | أداة ALTCHA مع حد معدل الطلبات وجدار WAF | إثبات العمل يرفع الكلفة، ولا يوقف الحجم وحده |
| تبحث عن رفع الصعوبة تلقائياً             | أداة mCaptcha بعد مراجعة حالتها          | الفكرة جيدة، ولكن المشروع لم يتحرك منذ سنة   |

## أخطاء شائعة

- **التحقق من التوقيع دون جدول المسائل المستخدمة:** حل واحد يكفي لآلاف النماذج كما رأيت.
- **الاعتماد على الواجهة وحدها:** الحقل المخفي لا يحمي شيئاً إذا لم يتحقق السيرفر منه، فمن يرسل النموذج من `curl` لا يمر بالواجهة أصلاً.
- **الصفحة على HTTP:** الواجهة تحتاج سياقاً آمناً Secure Context لتعمل، فظهرت في تجربتنا رسالة الخطأ «Secure context (HTTPS) required» عندما فتحنا النموذج دون HTTPS.
- **مفتاح HMAC في الكود أو في المستودع:** من يعرفه يستطيع أن يوقع مسائل سهلة لنفسه.
- **صلاحية طويلة للمسألة:** كلما طالت زاد وقت المهاجم لإعادة استخدام الحل، وزاد حجم جدول المسائل المستخدمة.
- **صعوبة لا تناسب أجهزة زوارك:** جرب النموذج على أضعف هاتف يستخدمه جمهورك قبل أن تختار القيمة.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- الخدمتان reCAPTCHA وTurnstile تعملان جيداً، ولكن متصفح كل زائر يتحدث مع شركة أخرى، وهذا يحتاج أن تذكره وقد لا يقبل في بعض الأنظمة.
- أداة ALTCHA برخصة MIT تجعل كل إرسال يكلف وقتاً من المعالج، ويتحقق منه سيرفرك وحده دون طرف ثالث.
- التوقيع يمنع تعديل المسألة، ولكنه لا يمنع إعادة استخدام الحل، فالجدول الذي يسجل المسائل المستخدمة ليس اختيارياً.
- أداة mCaptcha فكرة جيدة برخصة AGPL، ولكن إصداره الوحيد من 2024 ولم يتغير المستودع منذ سنة.
- إثبات العمل يرفع الكلفة على البرامج، ولا يغني عن حد معدل الطلبات وعن جدار WAF.

[![](https://arabroot.io/content/images/2026/10/anubis-cover-cover.webp)دليلحماية موقعك من روبوتات الذكاء الاصطناعي مع Anubisكيف يوقف Anubis روبوتات جمع البيانات بمسألة إثبات عمل، دون أن يحجب محركات البحث وقراء RSS عن موقعك.](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D9%85%D9%88%D9%82%D8%B9%D9%83-%D9%85%D9%86-%D8%B1%D9%88%D8%A8%D9%88%D8%AA%D8%A7%D8%AA-%D8%A7%D9%84%D8%B0%D9%83%D8%A7%D8%A1-%D8%A7%D9%84%D8%A7%D8%B5%D8%B7%D9%86%D8%A7%D8%B9%D9%8A-%D9%85%D8%B9-anubis/)[![](https://arabroot.io/content/images/2026/10/cloudflare-services-cover-cover.webp)دليلخدمات Cloudflare بكلمات بسيطة وبدائلها مفتوحة المصدرما الذي تقدمه كل خدمة من Cloudflare، وما المتاح منها مجاناً، وما البديل مفتوح المصدر الذي تشغله على سيرفرك.](https://arabroot.io/articles/%D8%AE%D8%AF%D9%85%D8%A7%D8%AA-cloudflare-%D9%88%D8%A8%D8%AF%D8%A7%D8%A6%D9%84%D9%87%D8%A7-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1/)[![](https://arabroot.io/content/images/2026/10/rate-limiting-cover-cover.webp)دليلتحديد معدل الطلبات Rate Limiting وخوارزمياتهكيف تعمل خوارزميات تحديد معدل الطلبات، وأيها يناسب واجهتك، وكيف تطبقها على عدة سيرفرات وترد بالرمز 429 الصحيح.](https://arabroot.io/articles/%D8%AA%D8%AD%D8%AF%D9%8A%D8%AF-%D9%85%D8%B9%D8%AF%D9%84-%D8%A7%D9%84%D8%B7%D9%84%D8%A8%D8%A7%D8%AA-rate-limiting-%D9%88%D8%AE%D9%88%D8%A7%D8%B1%D8%B2%D9%85%D9%8A%D8%A7%D8%AA%D9%87/)[![](https://arabroot.io/content/images/2026/10/self-hosted-waf-cover-cover.webp)دليلجدار WAF على سيرفرك: أي أداة تختار؟كيف تختار جدار حماية تطبيقات الويب لسيرفرك، ولماذا قد يرفض اسماً عربياً عادياً، وكيف تصلح ذلك دون أن تضعف الحماية.](https://arabroot.io/articles/%D8%AC%D8%AF%D8%A7%D8%B1-waf-%D8%B9%D9%84%D9%89-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D9%83-%D8%A7%D9%8A-%D8%A7%D8%AF%D8%A7%D8%A9-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1/)[![](https://arabroot.io/content/images/2026/10/accessibility-testing-cover-cover.webp)دليلاختبار سهولة الوصول Accessibility لموقع عربيما الذي تجده أدوات فحص سهولة الوصول وما الذي يفوتها، وما الذي يخص المواقع العربية، وكيف تمنع عودة المشاكل في خط CI.](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%B3%D9%87%D9%88%D9%84%D8%A9-%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-accessibility-%D9%84%D9%85%D9%88%D9%82%D8%B9-%D8%B9%D8%B1%D8%A8%D9%8A/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على Docker Engine 29.8.0 مع ALTCHA 3.3.0 وaltcha-lib 2.6.0 وNode.js 24.21.0 وNginx 1.31.6 وPlaywright 1.64.0 مع Chromium 156 و@axe-core/playwright 4.13.0.