المصيدة التي لم تصطد سواي: عندما يتظاهر فخ الرسائل المزعجة بالنجاح
جعفر أبازيد
كانت الرسائل المزعجة تنجح في المرور في كل مرة. أما الرسالة الوحيدة التي أوقفتها مصيدة الرسائل المزعجة لدينا، حسب علمنا، فكانت رسالتي أنا.
على مدى 100 يوم، منذ اليوم الذي أُطلق فيه نموذج التواصل على هذا الموقع في أيار/مايو وحتى نهاية آب/أغسطس، كان النموذج يحتوي على مصيدة للرسائل المزعجة Honeypot: حقل مخفي لا يراه الأشخاص، ولا تستطيع الروبوتات Bots، كما يُفترض، مقاومة ملئه. وعندما كان هذا الحقل يعود مملوءاً، كان الخادم يفترض أن المرسل روبوت، ويرد بالطريقة المعتادة لمصائد العسل: برسالة نجاح واضحة، حتى لا يحاول الروبوت مرة أخرى.
لم تصطد المصيدة يوماً الرسائل المزعجة التي صُممت من أجلها. وعندما اختبرت النموذج بنفسي أخيراً، اصطادتني أنا.
المصيدة
هذا ما كان موجوداً في أعلى نموذج التواصل، قبل حقل الاسم:
<div class="absolute left-[-9999px] top-[-9999px]" aria-hidden="true">
<label>
Company website
<input type="text" name="company_website" tabindex="-1" autocomplete="off" />
</label>
</div>
وهذا أول ما كان يفعله الخادم مع كل إرسال:
// Honeypot: bots fill hidden fields. Pretend success so they don't retry.
if ((data.company_website || "").trim()) {
return respond(wantsJson, true, "Thanks! Your message is on its way.", 200);
}
كل سطر من ذلك يمثل نصيحة شائعة. فالحقل يُدفَع إلى مسافة 9,999 بكسلاً خارج الصفحة، بدلاً من إخفائه، حتى ترى الروبوتات التي تقرأ الـ HTML حقلاً عادياً. وتمنع tabindex="-1" مستخدمي لوحة المفاتيح من الوصول إليه، بينما تُبقي aria-hidden قارئات الشاشة بعيدة عنه، ويطلب autocomplete="off" من المتصفح تركه وشأنه. وعلى الخادم، يُنفَّذ الفحص قبل التحقق من صحة البيانات وقبل إرسال أي شيء، ويُرسَل الرد نفسه الذي يحصل عليه المرسل الحقيقي عند النجاح، حتى لا تتعلم الروبوتات شيئاً.
تذكّروا الجزء الأخير. فهو القصة بأكملها.
كيف بدت الرسائل المزعجة فعلياً
في 8 آب/أغسطس، وصلت رسالة باللغة الليتوانية: “Sveiki, aš norėjau sužinoti jūsų kainą”، أي: “مرحباً، أردت معرفة سعركم”. وفي 11 آب/أغسطس، وصلت الرسالة مرة أخرى: الاسم نفسه، وعنوان Gmail نفسه، والمؤسسة نفسها، والرسالة نفسها، بايتاً ببايت. الشيء الوحيد الذي تغيّر هو الخيار “أنا مهتم بـ”، من “الوصول المبكر إلى NUZ” إلى “شراكة”. وفي 29 آب/أغسطس، وصلت رسالة ثالثة، باسم جديد، وعنوان ينتهي بـ.cim، ورابط.
لا يعيد شخص إرسال الجملة نفسها بعد ثلاثة أيام مع اختيار مختلف. أما البرنامج النصي Script الذي يتنقل بين خيارات القائمة المنسدلة Dropdown، فيفعل ذلك. كانت الرسالتان الأوليان لجسّ النبض، والتحقق من أن النموذج يصل إلى صندوق بريد إلكتروني حقيقي دون ارتداد. أما الرسالة الثالثة فكانت المحتوى الفعلي Payload.
وقد وصلت الرسائل الثلاث جميعاً إلى صندوق الوارد، ما يعني أن المصيدة كانت فارغة في المرات الثلاث. وأياً كان الشيء الذي أرسلها، فإنه لم يملأ الحقل المخفي. وهذا ما تتوقعه من برنامج نصي ينشئ الطلب بنفسه، انطلاقاً من أسماء الحقول التي يريدها، ثم يرسله مباشرة إلى نقطة النهاية Endpoint دون تحميل الصفحة على الإطلاق. أما الروبوت الذي يعرض النموذج ويملأ كل ما يجده، فكان سيقع في المصيدة. لكنه لم يرها أصلاً.
لا أستطيع إثبات أن البرنامج النصي لم يحمّل الصفحة أبداً. لكن كل شيء يشير إلى ذلك، وقد تبيّن أن هذا الأمر أهم من أي شيء آخر هنا.
ثم اصطادتني أنا
في 30 آب/أغسطس، كنت أختبر البديل على حاسوبي المحمول. فشل الإرسال الأول لسبب ممل: لم يكن هناك مفتاح بريد إلكتروني على جهازي، ولذلك توقف الخادم مباشرة بعد خطوة التحقق الجديدة. أما الإرسال الثاني، فعاد برسالة النجاح.
لم يُرسل أي شيء. وأظهر السجل نجاحاً مثل أي نجاح آخر. لكن ما كشف الأمر هو المدة التي استغرقها الإرسال:
| الإرسال | الاستجابة | الوقت |
|---|---|---|
| الأول | 500، توقف عند غياب مفتاح البريد الإلكتروني | 484 مللي ثانية |
| الثاني | 200، “شكراً! رسالتك في طريقها.” | 3 مللي ثانية |
أمضى الطلب الأول مدة 484 مللي ثانية في استدعاء خدمة التحقق التابعة لـ Cloudflare عبر الشبكة. أما 3 مللي ثانية، فلا تكفي لاستدعاء أي شيء. فالاستجابة بهذه السرعة لم تغادر الجهاز قط، وكان هناك مسار واحد فقط في الدالة Function يمكنه الرد برسالة نجاح دون اتصال بالشبكة: مصيدة العسل Honeypot.
أي أن متصفحي أنا هو من ملأ حقلاً لم أستطع رؤيته.
لماذا ملأ Chrome حقلاً لا يراه أحد
في ذلك الوقت، ألقيت اللوم على الملء التلقائي Autofill ومضيت. وأثناء كتابة هذا المقال، تحققت من الأمر، لأن “الملء التلقائي هو من فعل ذلك” كان استنتاجاً مبنياً على زمن الاستجابة، وليس شيئاً رآه أحد بالفعل. كان النموذج يمسح حقوله بعد نجاح الإرسال، ولذلك اختفت القيمة قبل أن أتمكن من النظر إليها.
أعدت بناء النموذج القديم كما هو تماماً، في صفحة تعرض ما كان سيُرسل بدلاً من إرساله، ثم ملأته بالطريقة التي سيفعلها العميل، من خلال اختيار البيانات التي حفظها Chrome:
| المصيدة | موضعها | ما فعله Chrome |
|---|---|---|
| ”Company website”، خارج الشاشة، كما في نموذجنا | الحقل الأول | ملأه باسم شركتي المحفوظ، وترك حقل المؤسسة الظاهر فارغاً |
| الحقل نفسه | الحقل الأخير | تركه فارغاً، وملأ الحقل الظاهر |
الحقل نفسه، مخفياً باستخدام display:none | الحقل الأول | تركه فارغاً |
| اسم وتسمية محايدان (“Leave this empty”)، خارج الشاشة | الحقل الأول | تركه فارغاً |
قرأ Chrome عبارة “Company website” باعتبارها حقل الشركة. ولأنها جاءت أولاً، قبل حقل “Organisation / website” الحقيقي، وضع Chrome اسم شركتي المحفوظ في المصيدة وترك الحقل الظاهر فارغاً. ومن موقعي، بدا النموذج كما ينبغي تماماً: اسمي، وبريدي الإلكتروني، وحقل اختياري متروك فارغاً. لم يكن هناك شيء يلفت الانتباه.
لم يُحدث autocomplete="off" أي فرق. وكذلك لم يُحدث دفع الحقل إلى خارج الشاشة أي فرق؛ فبالنسبة للمتصفح، يظل حقل إدخال يبعد 9,999 بكسلاً إلى اليسار حقلاً موجوداً في الصفحة. ما تطلّبه الأمر هو اجتماع عاملين: تسمية Label تبدو كشيء اعتاد المتصفح حفظه نيابةً عنك، وموضع يسبق الحقل الذي ينتمي إليه ذلك الشيء.
هذا متصفح واحد، مع ملف شخصي محفوظ واحد. لم أختبر Safari أو Firefox أو مديري كلمات المرور Password Managers، وأتوقع أن تختلف التفاصيل بينها. لكن الجوهر هو المهم: فالمصيدة التي تبدو كبيانات حقيقية سيملؤها، عاجلاً أو آجلاً، شيء يتصرف بحسن نية.
النجاح المزيف هو الخلل الحقيقي
لم تفشل مصيدة العسل لأنها اصطادت شخصاً. فكل مرشح للرسائل المزعجة قد يلتقط شخصاً حقيقياً في نهاية المطاف. لقد فشلت بسبب ما فعلته بعد ذلك: إذ أبلغت ذلك الشخص أن رسالته في طريقها، ثم تخلصت منها دون أن تترك أثراً. لم يظهر أي خطأ للزائر، ولم يُكتب أي سطر في السجل Logs لديّ، ولم يكن هناك شيء في صندوق الوارد يمكن أن ألاحظ غيابه.
صُمم ذلك النجاح المزيف للروبوتات، حتى لا تعاود المحاولة. لكن السطر نفسه الذي كان يهدف إلى خداع روبوت، خدع الزائر الوحيد الذي اصطادته المصيدة. وهذا يشبه تماماً الإصدار الذي لا يحمل عنواناً، وعملية الحفظ التي لم تؤدِّ إلى شيء في مقال رد 404 الوهمي: استجابة تبدو تماماً كأنها نجاح، صادرة عن مسار لم يفعل شيئاً.
لا نعلم بفقدان أي استفسار خلال تلك الأيام المئة. وهذه الجملة أقل طمأنة مما تبدو، لأن المسار الذي كان سيُضيّع استفساراً هو نفسه المسار الذي لا يترك أي أثر. والسبب الوحيد الذي يجعلنا نعرف بأمر اختباري أنا هو أنني كنت أراقب أزمنة الاستجابة مصادفة.
الحل البديل
Turnstile، البديل الذي تقدمه Cloudflare لـ CAPTCHA، يعمل في الوضع غير المرئي، ولذلك لا يراه الزائر إلا إذا قررت Cloudflare أنه يحتاج إلى إثبات شيء ما.
المهم هنا هو أين يتم إجراء التحقق. فإعداد البدء السريع لدى Cloudflare ينشر خدمة تحقق منفصلة تستدعيها الصفحة قبل إرسال النموذج، ما يترك نقطة النهاية الخاصة بالنموذج نفسه دون أي فحص. ولم يكن ذلك لينفع هنا، لأن مرسل الرسائل المزعجة لدينا لم يستخدم الصفحة إطلاقاً. لقد أرسل الطلب مباشرة إلى نقطة النهاية، ولذلك فإن الفحص الذي تجريه الصفحة هو فحص لا يخضع له مطلقاً.
لذلك يجري التحقق داخل الدالة التي تستقبل النموذج، مع كل طلب، وقبل إرسال أي شيء. ويُرفض أي طلب لا يتضمن رمزاً Token صالحاً، أياً كان مرسله وطريقة وصوله. اختبرت الحل باستخدام بنية الطلب نفسها التي استخدمها مرسل الرسائل المزعجة، فعادت الاستجابة 400. ومنذ تطبيق هذا الحل في 30 آب/أغسطس، لم تصل أي رسائل شبيهة بالرسائل الثلاث.
وهذا كلّفنا أمراً واحداً: لم يعد النموذج يعمل دون JavaScript، لأن Turnstile يحتاج إليه لإصدار الرمز Token. وقد قبلنا ذلك، وأزلنا مصيدة العسل في الوقت نفسه بدلاً من الإبقاء عليها كطبقة ثانية. فالشيء الوحيد الذي رأيناها تفعله هو إيقاف رسالة حقيقية.
إذا كنت تستخدم مصيدة مماثلة
يوفر كثير من إضافات Plugins النماذج في ووردبريس مصيدة كهذه، كما أن كثيراً من المواقع يبني مصيدة خاصة به، كما فعلنا نحن. لم أختبر هذه الإضافات، ولذلك لن أذكر أسماء أيٍّ منها. لكن هذه الفحوص تنطبق عليها جميعاً:
- املأ نموذجك بنفسك باستخدام الملء التلقائي. لا تفعل ذلك بالكتابة؛ بل اختر البيانات التي حفظها متصفحك، بالطريقة التي قد يفعلها العميل، وتحقق مما يصل.
- اكتشف ما تفعله المصيدة عندما تُفعَّل. إذا كانت الإجابة هي “تعرض نجاحاً وتتخلص من الرسالة”، فغيّر ذلك. سجّل عملية الاصطياد، وأعِد الاستجابة نفسها التي يحصل عليها الطلب عند فشل الفحص، حتى يكون الإنذار الكاذب False Positive مرئياً على الأقل.
- سمِّها بطريقة لا توحي بشيء. فالتسمية واسم الحقل اللذان يوحيان ببيانات حقيقية، مثل الشركة أو الموقع الإلكتروني أو الهاتف أو العنوان، هما بالضبط ما يبحث عنه الملء التلقائي. وقد بقيت المصيدة ذات الاسم المحايد فارغة في اختبارنا.
- انتبه إلى المفاضلة في طريقة الإخفاء. أبقى
display:noneمتصفح Chrome بعيداً، بخلاف الحقل الموجود خارج الشاشة. ويُوصى عادةً بوضع الحقل خارج الشاشة، لأن بعض الروبوتات تتجاوز الحقول المخفية باستخدامdisplay:none. وهذه هي المفاضلة: التقنية التي تهدف إلى خداع عدد أكبر من الروبوتات هي نفسها التي خدعت Chrome. - راقب أزمنة الاستجابة. النجاح الذي يعود أسرع من أبطأ خطوة حقيقية لديك، كإرسال بريد إلكتروني أو استدعاء واجهة برمجية API، هو الذي لم ينفذ تلك الخطوة أصلاً.
- تحقق من المكان الذي تصل إليه البيانات، لا من المكان الذي توجد فيه الصفحة. إن كانت الرسائل المزعجة تُرسَل مباشرة إلى نقطة النهاية وليس من الصفحة التي تستضيف نموذج التواصل، فكل ما يعمل داخل المتصفح هو مجرد زينة.
الجزء الذي لا يفارقني
كان كل سطر من أسطر تلك المصيدة خطوة جيدة بحد ذاته. إخفاؤها عن الأشخاص، وإبعادها عن قارئات الشاشة، وطلب عدم ملئها من المتصفح، وترك الروبوت يظن أنه نجح. لكنها مجتمعة بنت شيئاً يستطيع أن يكذب بهدوء على شخص حقيقي، في حين كانت الرسائل المزعجة التي صُممت المصيدة من أجلها تمر من أمامها دون اعتراض.
لم تكن مصيدة الرسائل المزعجة مخطئة بشأن الروبوتات. بل كانت مخطئة بشأن من يملأ النماذج سوى الروبوتات.