كل آلية تحكم لكل IP شحنّاها كانت قابلة للتجاوز بترويسة واحدة
كان عدّاد قفل الحسابات لدينا يوقف محاولات تسجيل الدخول الفاشلة عند خمس. وكان الـ rate limiter يكبح العملاء المسيئين. كلاهما كان يعتمد على عنوان IP الخاص بالعميل، وهو الشيء البديهي الذي تعتمد عليه. وكلاهما كان قابلاً للتجاوز بالكامل، وكان التجاوز ترويسة HTTP واحدة يمكنك ضبطها بنفسك.
وإليك السبب. عندما يعمل تطبيقك خلف reverse proxy، وتطبيقنا يعمل خلف nginx على Kubernetes، فإن اتصال TCP الذي يراه التطبيق لا يأتي من المستخدم. إنه يأتي من الـ proxy. كل طلب يصل بنفس عنوان الطرف الآخر للـ socket: الـ ingress. لذلك يجب أن يُحمَل عنوان IP الحقيقي للعميل في ترويسة، هي X-Forwarded-For، يضبطها الـ proxy ويقرأها التطبيق. ولدى ASP.NET Core حزمة middleware لهذا الغرض تحديداً: تقرأ X-Forwarded-For وتعيد كتابة عنوان IP البعيد للاتصال ليطابقها، فيرى بقية كودك، بما في ذلك الـ rate limiter، العميل الحقيقي.
المشكلة أن X-Forwarded-For مجرد ترويسة. يمكن لأي أحد إرسالها. إذا وثق التطبيق بها دون شروط، وتطبيقنا فعل ذلك، واثقاً بها من أي مصدر كان، فإن قيمتها تحت سيطرة المهاجم. لذا فالهجوم ليس ذكياً، إنه سطر واحد:
curl -H "X-Forwarded-For: 10.0.0.1" https://.../login
curl -H "X-Forwarded-For: 10.0.0.2" https://.../login
curl -H "X-Forwarded-For: 10.0.0.3" https://.../login
غيّر الترويسة مع كل طلب وسيظن التطبيق أن كل محاولة تأتي من عميل جديد تماماً. عدّاد القفل، المعتمد على ذلك العنوان، لا يصل أبداً إلى خمس لأي قيمة بعينها. ودلو الـ rate limiter جديد في كل مرة. يمكنك تجربة كلمات المرور بالقوة الغاشمة طوال اليوم، وكل دفاع لكل IP لدينا يعدّ بسعادة حتى واحد. آلية تحكم تعتمد على قيمة يضبطها المهاجم ليست آلية تحكم.
يبدو الإصلاح تافهاً: لا تثق في X-Forwarded-For إلا من الـ proxy الخاص بك. إنه تافه من حيث المبدأ ومتعب في التطبيق العملي، وKubernetes هو موطن هذا التعب.
لا يمكنك ببساطة التوقف عن قراءة الترويسة. إن فعلت، سيبدو كل طلب وكأنه قادم من الـ ingress، فيتشارك كل عملائك عنوان IP واحداً وتنكسر آليات التحكم لكل IP في الاتجاه المعاكس: مسيء واحد يفعّل القفل على الجميع. عليك أن تقرأ X-Forwarded-For، لكن لا تصدقها إلا حين يكون الطلب قد جاء فعلاً من proxy تثق به.
حزمة middleware الترويسات المُمرَّرة في ASP.NET تثق بحسب المصدر: قائمة بعناوين IP لخوادم proxy معروفة، أو شبكات proxy معروفة على شكل CIDRs. الخطوة الساذجة هي تثبيت عنوان IP الخاص بـ pod الـ ingress. على AKS ذلك العنوان غير مستقر. تُعاد جدولة pods الـ ingress ويُعاد توسيعها، وتتبدل عناوينها معها، فيصبح العنوان المثبَّت قديماً وإما أن يكسر الحركة المشروعة أو، إن تركت احتياطياً قديماً، يعيد فتح الثغرة. المستقر هو الشبكة الفرعية التي تسحب منها الـ pods عناوينها. لذا تثق في CIDR الخاص بالشبكة الفرعية للعقد، لا في أي عنوان مفرد. كل قفزة يحق لها شرعاً ضبط الترويسة تعيش داخل ذلك النطاق، ولا يُصدَّق شيء خارجه.
ثم هناك العدد. الـ middleware يمشي على قائمة X-Forwarded-For من اليمين، مقشّراً قفزة موثوقة واحدة في كل مرة، وما يتبقى بعد القفزات الموثوقة يُعتبَر هو العميل. امشِ قفزة واحدة زيادة وستتجاوز الـ proxy الخاص بك إلى الجزء الذي كتبه المهاجم من الترويسة. لذا يجب أن يساوي عدد القفزات التي تقشّرها عدد خوادم الـ proxy التي تضيف فعلاً إلى الترويسة، بالضبط. في مسارنا هناك واحد: nginx. موازن حمل Azure الذي يقف أمامه يعمل في الطبقة الرابعة. إنه يمرر TCP ولا يحلل HTTP، ولا يضيف أي مدخل X-Forwarded-For. لذا فحد التمرير الصحيح هو واحد. ليس القيمة الافتراضية، وليس «بضع قفزات للاحتياط». واحد، لأن proxy واحداً فقط يلمس تلك الترويسة.
هذا الزوج، الثقة في الشبكة الفرعية للعقد وتقشير قفزة واحدة بالضبط، هو الإصلاح كله. عنوان IP للعميل الذي يسلمك إياه الـ middleware هو الآن القيمة التي كتبها nginx من الـ socket الحقيقي، ومدخلات المهاجم المحقونة تجلس على يسارها، متجاهَلة، لأننا نتوقف عن المشي قبل أن نصل إليها.
الجزء القابل للنقل ليس الأرقام، بل كونها أرقاماً عن طوبولوجيتك أنت تحديداً. X-Forwarded-For ليست بيانات، إنها تفويض ثقة: قراءتها تعني قول «أنا أصدق من ضبط هذا». وهذا التصديق يجب تثبيته على القفزات الدقيقة في مسار طلباتك، لا أوسع ولا أضيق. ثق على نطاق أوسع من اللازم وتصبح قابلة للتزوير. ثق على نطاق أضيق من اللازم وتنهار هوية كل عملائك في الـ proxy. والعدد مقترن ببنيتك التحتية، فاليوم الذي تضع فيه CDN أمام كل شيء تكون قد أضفت قفزة، وحد التمرير الذي كان صحيحاً بالأمس صار اليوم ناقصاً بواحد. الإصلاح ليس ثابتاً سحرياً. إنه: عُدّ خوادم الـ proxy في مسارك، وثق بها وحدها دون سواها، وأعد العدّ كلما تغير المسار.
عدّ قفزات الـ proxy بشكل صحيح عمل بلا بريق، يسهل الخطأ فيه، وهو الذي يقرر ما إذا كانت حدود المعدل لديك تعني أي شيء على الإطلاق. Authagonal يتولى هذه المحاسبة نيابة عنك، فيَعُدّ قفل الحسابات لديك العميل لا ترويسة.