استراتيجيات كشف المتسللين مبكراً في تطبيقاتك البرمجية
لماذا يجب رصد المحاولات الخبيثة؟ ألا يكفي منع الثغرات الأمنية؟
قد تقول: “طالما أنني أكتب شيفرة آمنة، لا يهمني إذا كان المتسللون يعبثون ببرنامجي القوي أم لا. فلماذا أهتم بالمحاولات الخبيثة؟” دعنا نجيب أولاً على هذا السؤال الوجيه. من الصعب الحفاظ على أمان قطعة برمجية معقدة نسبياً طوال الوقت. فكلما زادت التعقيدات، زادت نقاط الضعف المحتملة التي يمكن للمتسلل استغلالها أثناء تصميم الشيفرة أو تنفيذها أو نشرها أو صيانتها. ما عليك سوى إلقاء نظرة على أعداد ثغرات CVE على مر السنين. إنها كثيرة:
عدد الثغرات الأمنية المنشورة على مر السنين بواسطة موقع cvedetails.com.
علاوة على ذلك، وبسبب طبيعتها، فإن الثغرة الأمنية ليست مجرد عنصر عادي في قائمة مهامك المتأخرة. هناك عواقب وخيمة إذا تم استغلال إحدى الثغرات: فقدان الثقة، سمعة سيئة، أو حتى خسارة مالية. لذلك، توجد أفضل الممارسات الأمنية مثل معيار التحقق من أمان تطبيقات OWASP ASVS (Application Security Verification Standard) أو إرشادات Mozilla للترميز الآمن (Secure Coding Guidelines) لمساعدة المطورين على إنتاج برامج آمنة. ومع ذلك، نظراً لظهور طرق جديدة لتجاوز ضوابط الأمان الحالية أو نقاط ضعف جديدة بشكل شبه يومي، هناك إجماع داخل مجتمع الأمن على أنه “لا يوجد أمان بنسبة 100%.” لذا يجب علينا دائماً أن نكون يقظين ومستجيبين لأخبار وتحسينات الأمان.
هناك أيضاً أمر آخر يمكننا فعله لضمان برامج آمنة: ملاحظة المتسللين في أقرب وقت ممكن، قبل أن يفعلوا شيئاً لا نتوقعه أو حتى لا نعرف عنه. علاوة على ذلك، فإن تتبع سلوكهم الخبيث على مدى فترة طويلة يجعلنا أكثر استباقية. هناك مفهوم شائع لمركز عمليات الأمن (Security Operations Center - SOC) في هذا السياق – مراكز SOC هي نوع من الفرق داخل المؤسسة، سواء كانت داخلية أو خارجية. تتمثل مهمتهم في مراقبة الحالة الأمنية للمؤسسة بشكل مستمر، وذلك من خلال اكتشاف حوادث الأمن السيبراني وتحليلها والاستجابة لها. تبحث فرق SOC عن الأنشطة غير الطبيعية، بما في ذلك الشذوذات الأمنية في البرمجيات. إن فكرة ملاحظة الهجمات السيبرانية الناجحة أو الفاشلة والاستجابة لها تمنح المؤسسات ميزة ضد التهديدات، مما يقلل في النهاية من وقت الاستجابة للهجمات من خلال المراقبة المستمرة. يكون مركز SOC قوياً فقط بالمدخلات الغنية والجودة التي يتلقاها من مصادر مختلفة لمكونات تكنولوجيا المعلومات. وبما أن برامجنا تعد جزءاً مهماً من المخزون، فإن تنبيهات الأمان المناسبة الناتجة عن السلوكيات غير الطبيعية التي ترسلها برامجنا إلى فرق SOC لا تقدر بثمن.
كيفية التحقق من السلوكيات غير الطبيعية
فيما يلي عدد من الفحوصات والضوابط التي يمكننا تطبيقها في شيفرتنا البرمجية للكشف عن السلوكيات الخبيثة وغير الطبيعية. قبل البدء، أود التأكيد على أنني لا أقدم هنا حلولاً معقدة مثل جدار حماية تطبيقات الويب (Web Application Firewall - WAF). بدلاً من ذلك، سأحاول أن أوضح لك أن الشروط البسيطة، والمعالجة الذكية للاستثناءات، والإجراءات المشابهة التي لا تتطلب جهداً كبيراً في شيفرتك يمكن أن تساعدك في ملاحظة السلوكيات غير الطبيعية بمجرد حدوثها. دعنا نتعمق.
القيم الفارغة أو المرتجعات ذات الطول الصفري
الإجراء الأول الذي يمكننا اتخاذه للكشف عن عمل خبيث هو التحقق من التجميعات ذات الطول الصفري أو القيم المرتجعة الفارغة (null). إليك كتلة شيفرة بسيطة لتوضيح الفكرة:
Receipt receipt = GetReceipt(transferId);
if (receipt == null )
{
// what does this mean?
// log, notify, alarm
}
هنا، نحاول الوصول إلى إيصال تحويل معين يقدمه المستخدمون النهائيون عبر المعامل transferId. لمنع أي شخص من الوصول إلى إيصالات شخص آخر، دعنا نفترض أنه داخل الدالة GetReceipt، المطور ذكي بما يكفي للتحقق مما إذا كان المعامل transferId ينتمي بالفعل للمستخدم الحالي. التحقق من الملكية هو ممارسة أمنية جيدة. دعنا نفترض أيضاً أننا متأكدون بالتصميم أن كل تحويل يجب أن يكون له إيصال واحد على الأقل، لذا فإن عدم الحصول على أي إيصال أثناء التشغيل أمر مشبوه. لماذا؟ لأن الحصول على إيصال فارغ يعني أن المعامل transferId المقدم لا ينتمي إلى أي تحويل نفذه المستخدم الحالي. بمعنى آخر، قدم المستخدم الحالي معامل transferId مزوراً لشيفرتنا وينتظر رؤية المحتوى إذا كان هذا المعامل transferId يتعلق بمعاملة شخص آخر. وبما أن لدينا التحكم المناسب في الملكية، فإن الدالة GetReceipt تُرجع إيصالاً فارغاً أو null. هنا يجب علينا اتخاذ بعض الإجراءات الأمنية. لن أخوض في تفاصيل الإجراءات الأمنية في هذا المقال. ومع ذلك، فإن تسجيل الأحداث الأمنية و/أو إرسال إشعارات مفصلة، وأنظمة إدارة معلومات وفعاليات الأمن (Security Information and Event Management - SIEM) هما اثنان من هذه الإجراءات.
إليك مثال آخر يوضح كيف يسمح لنا التحقق من قيمة null باكتشاف محاولة خبيثة. لنفترض أن لدينا نقاط النهاية (endpoints) الثلاثة التالية: ShowReceipt، Success، و Error:
// ShowReceipt endpoint
if (CurrentUser.Owns(receiptId))
{
Session[ "receiptid" ] = receiptId;
redirect "Success" ;
}
else
{
redirect "Error" ;
}
// Success endpoint
receiptId = Session[ "receiptid" ];
return ReadReceipt(receiptId);
// Error endpoint
return "Error" ;
هذا تطبيق بسيط يعرض محتوى إيصال المستخدم. في نقطة النهاية ShowReceipt، السطر الأول مهم. إنه يتحقق مما إذا كان المستخدم النهائي يرسل لنا receiptId صالحاً لرؤية محتوياته. بدون هذا التحكم، يمكن لمستخدم خبيث تقديم أي receiptId والوصول إلى المحتوى. مكان العبارة في السطر الثالث لا يقل أهمية. إذا نقلنا هذا السطر قبل عبارة if مباشرة، فلن يكسر ذلك أي شيء. ومع ذلك، فإنه سيخلق نفس المشكلة الأمنية التي كنا نحاول تجنبها عن طريق التحقق مما إذا كان المستخدم النهائي يطلب إيصالاً صالحاً أم لا. يرجى أخذ لحظة للتأكد من فهمك لسبب ذلك. الآن، من الجيد أننا وضعنا هذا السطر في مكانه الصحيح، وهذا يخلق فرصة أخرى لملاحظة المحاولات الخبيثة. ثم، في نقطة النهاية Success، ماذا يعني إذا حصلنا على قيمة receiptId فارغة (null) من Session؟ هذا يعني أن شخصاً ما يستدعي نقطة النهاية هذه، بعد أن قام بطلب إلى نقطة النهاية ShowReceipt باستخدام receiptId لشخص آخر. حتى لو تم توجيههم إلى Error بسبب فحص الملكية! بالطبع، مع التحكم الذي لدينا في السطر الأول، هذا مستحيل. لذا، فإن نقطة النهاية Success هي مكان جيد لكتابة سجل أمني وإرسال أي إشعارات إلى حلول المراقبة لدينا عندما نحصل على receiptId بقيمة null من Session.
// Success endpoint (Revisited)
receiptId = Session[ "receiptid" ];
if (receiptId == null )
{
// log, notify, alarm
}
return ReadReceipt(receiptId);
المعالجة الموجهة للاستثناءات
تعد معالجة الاستثناءات (Exception Handling) ربما الآلية الأكثر أهمية للمطورين للاستجابة لأي حالة شاذة أثناء تنفيذ البرنامج. في معظم الأحيان، تتيح الفرصة الرئيسية التي توفرها تنظيف الموارد المستعارة مثل تدفقات الملفات/الشبكة أو اتصالات قواعد البيانات عند حدوث مشاكل غير متوقعة. هذا سلوك آمن ضد الفشل يسمح لنا بكتابة برامج أكثر موثوقية. بالتوازي، يمكننا استخدام استثناءات وقت التشغيل (runtime exceptions) بفعالية لملاحظة المحاولات الخبيثة تجاه برامجنا. فيما يلي بعض مصادر الضعف الشائعة حيث يمكننا الاستفادة من الاستثناءات ذات الصلة لملاحظة السلوك المشبوه:
- إلغاء التسلسل (
Deserialization) - التشفير (
Cryptography) - تحليل
XML - التعبيرات النمطية (
Regular Expression) - العمليات الحسابية (
Arithmetic Operations)
القائمة ليست كاملة بالطبع. وهنا سأتناول فقط عدداً قليلاً من واجهات برمجة التطبيقات (APIs) هذه. دعنا نبدأ بالتعبيرات النمطية (Regular Expressions). إليك كتلة شيفرة تطبق طريقة تحقق صارمة على مدخلات المستخدم:
if (!Regex.IsMatch(query.Search, @"^([a-zA-Z0-9]+ ?)+$" ))
{
return RedirectToAction( "Error" );
}
نمط التعبير النمطي (Regular Expression) المستخدم هنا هو نمط قائمة بيضاء (whitelist) قوي، مما يعني أنه يتحقق مما هو متوقع كمدخل. وليس الطريقة الأخرى غير الآمنة، وهي التحقق مما هو معروف بأنه سيء. ومع ذلك، إليك نسخة أكثر أماناً من نفس كتلة الشيفرة:
if (!Regex.IsMatch(query.Search, @"^([a-zA-Z0-9]+ ?)+$" , RegexOptions.Compiled, TimeSpan.FromSeconds( 10 )))
{
return RedirectToAction( "Error" );
}
هذه نسخة محملة بزيادة (overloaded) من الدالة IsMatch، حيث المعامل الأخير هو المفتاح. إنه يفرض أن تنفيذ التعبير النمطي أثناء وقت التشغيل لا يمكن أن يتجاوز 10 ثوانٍ. إذا تجاوز ذلك، فهذا يعني أن شيئاً مشبوهاً يحدث، نظراً لأن النمط المستخدم ليس معقداً إلى هذا الحد. هناك نقطة ضعف أمنية فعلية قد تُستخدم لاستغلال هذا النمط تسمى ReDoS (Regular Expression Denial of Service)، على الرغم من أنني لن أخوض في تفاصيلها هنا. ولكن باختصار، يمكن للمستخدم النهائي إرسال السلسلة التالية كمعامل بحث وجعل نظامنا الخلفي (back-end) يعاني، مستهلكاً كمية هائلة من طاقة وحدة المعالجة المركزية (CPU) دون جدوى. لاحظ علامة الاقتباس في النهاية (ولا تجرب هذا في بيئة الإنتاج!):
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!=
السؤال هو، ماذا يحدث عندما يتجاوز وقت التنفيذ 10 ثوانٍ بالفعل؟ بيئة .NET تُطلق استثناءً، وهو RegexMatchTimeoutException. لذا، إذا قمنا بالتقاط هذا الاستثناء تحديداً، فلدينا الآن الفرصة للإبلاغ عن هذا الحادث المشبوه أو اتخاذ إجراء بشأنه. إليك كتلة الشيفرة النهائية لتحقيق ذلك:
try
{
if (!Regex.IsMatch(query.Search, @"^([a-zA-Z0-9]+ ?)+$" , RegexOptions.Compiled, TimeSpan.FromSeconds( 10 )))
{
return RedirectToAction( "Error" );
}
}
catch (RegexMatchTimeoutException rmte)
{
// log, notify, alarm
}
مجال آخر مهم يمكننا فيه الاستفادة من الاستثناءات هو تحليل XML. إليك كتلة شيفرة كمثال:
XmlReader xmlReader = XmlReader.Create(input);
var root = XDocument.Load(xmlReader, LoadOptions.PreserveWhitespace);
يتم تغذية مدخل XML إلى الدالة XmlReader.Create، ثم نحصل على العنصر الجذر. يمكن للمتسللين استغلال هذه القطعة من الشيفرة عن طريق توفير بعض ملفات XML الخبيثة، والتي عند تحليلها بواسطة الشيفرة أعلاه، تمنحهم ملكية خوادمنا. مخيف، أليس كذلك؟ تسمى الثغرة الأمنية هجوم كيان XML الخارجي (XML External Entity - XXE)، وكما هو الحال مع استغلال التعبيرات النمطية، لن أخوض في جميع التفاصيل هنا. ومع ذلك، لمنع نقطة الضعف الحرجة للغاية هذه، نتجاهل استخدام تعريفات نوع المستند (Document Type Definitions - DTD) من خلال إعدادات XmlReaderSettings. لذا الآن، لا توجد إمكانية لوجود ثغرات أمنية من نوع XXE بعد الآن. إليك النسخة الآمنة:
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Ignore;
XmlReader xmlReader = XmlReader.Create(input, settings);
var root = XDocument.Load(xmlReader, LoadOptions.PreserveWhitespace);
يمكننا ترك الشيفرة على هذا النحو والمضي قدماً. ومع ذلك، إذا حاول متسلل استغلال هذا الهجوم دون جدوى، فمن الأفضل أن نتمكن من اكتشاف هذا السلوك وإنتاج تنبيه أمني لا يقدر بثمن:
try
{
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Ignore;
XmlReader xmlReader = XmlReader.Create(input, settings);
var root = XDocument.Load(xmlReader, LoadOptions.PreserveWhitespace);
}
catch (XmlException xe)
{
// log, notify, alarm
}
علاوة على ذلك، لمنع الإيجابيات الكاذبة (false positives)، يمكنك تخصيص كتلة catch بشكل أكبر باستخدام محتوى الرسالة الذي يوفره كائن XmlException. هناك ممارسة برمجية عامة توصي بعدم استخدام أنواع الاستثناءات العامة (generic Exception types) في كتل catch. ما عرضناه هو أيضاً حالة داعمة جيدة لذلك. وينطبق الشيء نفسه على ممارسة أخرى توصي بعدم استخدام كتل catch الفارغة، والتي لا تفعل شيئاً فعلياً عندما يحدث سلوك غير طبيعي في شيفرتنا. ولكن يبدو أنه بدلاً من كتل catch الفارغة، لدينا هنا فرصة قوية جداً للرد على المحاولات الخبيثة.
تطبيع المدخلات (Normalization)
بحسب التعريف، التطبيع (normalization) هو الحصول على أبسط شكل لشيء ما. في الواقع، canonicalization هو المصطلح المستخدم لهذا الغرض. لكنه صعب النطق، لذا دعنا نلتزم بـ normalization. بالطبع، “أبسط شكل لشيء ما” هو أمر مجرد بعض الشيء. ماذا نعني بـ “أبسط شكل”؟ من الجيد دائماً التوضيح بالأمثلة. إليك سلسلة نصية: %3cscript%3e. وفقاً لترميز URL، هذه السلسلة ليست في أبسط أشكالها. لأنه إذا طبقنا فك ترميز URL عليها، نحصل على هذه: <script>. هذا هو أبسط شكل للسلسلة الأصلية وفقاً لمعيار تحويل ترميز URL. كيف نعرف ذلك؟ لا نعرفه لأنه أصبح مفهوماً لنا الآن. نعرفه لأنه إذا طبقنا فك ترميز URL مرة أخرى، فسنحصل على نفس السلسلة: <script>. وهذا يعني أن فك ترميز URL لم يعد يحولها بنجاح. لقد وصلنا إلى أبسط شكل.
يمكن أن يستغرق التطبيع أكثر من خطوة واحدة، حيث قد يتم تطبيق الترميز في الأصل أكثر من مرة. ترميز URL هو مجرد مثال واحد للتحويل المستخدم للتطبيع، أو بعبارة أخرى، فك الترميز (decoding). ترميز HTML، ترميز JavaScript، وترميز CSS هي طرق ترميز/فك ترميز أخرى مهمة تستخدم على نطاق واسع للتطبيع. على مر السنين، يجد المهاجمون تقنيات حقيقية لتجاوز أنظمة الدفاع. وإحدى التقنيات الأكثر انتشاراً التي يستخدمونها هي الترميز. يستخدمون تقنيات ترميز معقدة على مدخلاتهم الخبيثة الأصلية، من أجل خداع الدفاعات حول التطبيقات. التاريخ مليء بهذه الأمثلة، ويمكنك قراءة تفاصيل أحد أشهرها والذي يسمى هجوم IIS dotdot سيء السمعة من Microsoft الذي حدث في أوائل الألفينيات. بما أن المتسللين يعتمدون بشكل كبير على تقنيات الترميز عند إرسال مدخلات خبيثة، يمكن أن يكون التطبيع أحد أكثر الطرق فعالية وسهولة لاكتشافهم. إليك القاعدة الأساسية: نطبق فك ترميز URL/HTML/CSS/JavaScript بشكل متكرر على مدخلات المستخدم حتى لا يتغير الإخراج بعد ذلك. وإذا كان الإخراج سلسلة نصية مختلفة عن المدخل الأصلي، فهذا يعني أننا قد نواجه طلباً خبيثاً محتملاً. إليك نسخة مبسطة من مكتبة OWASP ESAPI Java الشهيرة التي تطبق هذه الفكرة:
int foundCount = 0 ;
boolean clean = false ;
while (!clean)
{
clean = true ;
// whatever codes you want; URL/Javascript/HTML/...
Iterator i = codecs.iterator();
while (i.hasNext())
{
Codec codec = (Codec)i.next();
String old = input;
input = codec.decode(input);
if (!old.equals(input))
{
if (clean)
{
foundCount++;
}
clean = false ;
}
}
}
عندما تنتهي كتلة الشيفرة، إذا كانت قيمة foundCount أكبر من أو تساوي 2، فماذا يعني ذلك؟ يعني أن شخصاً ما يرسل مدخلات متعددة الترميز إلى تطبيقنا، واحتمال حدوث ذلك نادر جداً. المستخدمون العاديون لا يرسلون سلاسل نصية متعددة الترميز إلى تطبيقنا. هناك احتمال كبير أن يكون هذا مستخدماً خبيثاً. يجب علينا تسجيل هذا الحدث مع المدخل الأصلي لتحليله لاحقاً. الآلية المذكورة أعلاه، على الرغم من كونها جزءاً من البرنامج نفسه، تعمل كمرشح أمام التطبيق. إنها تعمل على كل مدخل غير موثوق به وتمنحنا فرصة لمعرفة المحاولات الخبيثة. ومع ذلك، قد تشك في التأخير الإضافي الذي تسببه طريقة التحقق هذه. أتفهم إذا كنت لا ترغب في الاشتراك.
إليك مثال آخر على استخدام التطبيع كوسيلة لاكتشاف المحاولات الخبيثة أثناء تحميل أو تنزيل الملفات. لنفترض الشيفرة التالية:
if (!String.IsNullOrEmpty(fileName))
{
fileName = new FileInfo(fileName).Name;
string path = @"E:\uploaded_files\" + fileName;
if (File.Exists(path))
{
response.ContentType = "image/jpg" ;
response.BinaryWrite(File.ReadAllBytes(path));
}
}
هنا نحصل على المعامل fileName من عميلنا، نحدد الصورة التي يشير إليها، نقرأ المحتوى ونقدمه. هذا مثال على التنزيل. قد يكون أيضاً سيناريو تحميل. ومع ذلك، ولمنع العميل من التلاعب بالمعامل fileName كما يشاء، نستخدم خاصية Name من الفئة FileInfo. سيؤدي هذا إلى الحصول على جزء الاسم فقط من fileName، حتى لو أرسل لنا العميل أي شيء آخر غير ما نتوقعه (أي اسم ملف بمسارات مزورة مثل ما يلي):
../../WebSites/Cross/Web.config
هنا، يريد العميل الخبيث قراءة محتويات ملف Web.Config حساس باستخدام شيفرتنا. بمجرد الحصول على جزء اسم الملف فقط، نتخلص من هذه الإمكانية. هذا جيد ولكن لا يزال هناك شيء يمكننا فعله:
if (!String.IsNullOrEmpty(fileName))
{
string normalizedFileName = new FileInfo(fileName).Name;
if (normalizedFileName != fileName)
{
// log, notify, alarm
response = ResponseStatus.Unauthorized;
}
string path = @"E:\uploaded_files\" + fileName;
if (File.Exists(path))
{
response.ContentType = "image/jpg" ;
response.BinaryWrite(File.ReadAllBytes(path));
}
}
نقارن النسخة المطبعة من fileName بنفسها (المدخل الأصلي). إذا اختلفا، فهذا يعني أن شخصاً ما يحاول إرسال fileName معدل إلينا ونتخذ الإجراء المناسب. عادةً ما يرسل المتصفح اسم الملف المحمل في أبسط أشكاله دون أي تحويل. لأجل النقاش، قد لا نستخدم اسم الملف عندما يقوم المستخدم بتحميل ملف. قد نكون نولد معرفاً فريداً عالمياً (GUID) ونستخدمه بدلاً من ذلك. ومع ذلك، لا يزال تطبيق هذا التحكم على اسم الملف المقدم مهماً، لأن المتسللين سيحاولون بالتأكيد العبث بهذا المعامل مهما كان الأمر.
المدخلات غير الصالحة مقابل القوائم البيضاء (Whitelists)
القائمة البيضاء (Whitelisting) تعني “قبول ما هو متوقع فقط”. بمعنى آخر، إذا واجهنا مدخلاً لا نتوقعه، فإننا نرفضه. تعد استراتيجية التحقق من المدخلات هذه واحدة من أكثر الاستراتيجيات أماناً وفعالية لدينا حتى الآن. باستخدام هذه الاستراتيجية باستمرار في جميع أنحاء برنامجك، يمكنك إغلاق العديد من الطرق المعروفة وغير المعروفة التي يمكن للمستخدم الخبيث مهاجمتك من خلالها. هذه الطريقة في بناء البرامج تشبه بناء قلعة مغلقة بأبواب محكمة التحكم تفتح فقط إلى الخارج، إذا كان ذلك منطقياً. حسناً، نعود إلى موضوعنا. دعنا نحلل القائمة البيضاء (whitelisting) بسيناريو بسيط. لنفترض أن مستخدمينا لديهم حرية اختيار أسماء مستخدمين خاصة بهم عند التسجيل. وقبل الترميز، تم إبلاغنا كمتطلب كيف يجب أن يبدو اسم المستخدم. ثم، للامتثال لهذا المتطلب، يمكننا بسهولة وضع بعض القواعد الصارمة لتطبيقها على مدخل اسم المستخدم قبل قبوله. إذا اجتاز المدخل الاختبار، نقبله. وإلا، نرفض المدخل. قد تكون قواعد القائمة البيضاء (whitelist) بأشكال مختلفة، مع ذلك. قد يحتوي بعضها على قائمة من القيم المتوقعة المكتوبة بشكل ثابت (hard-coded values)، وقد يتحقق البعض الآخر مما إذا كان المدخل عدداً صحيحاً أم لا. وقد يكون البعض الآخر على شكل تعبيرات نمطية (regular expressions). إليك مثال على تعبير نمطي لأسماء المستخدمين: ^[a-zA-Z0-9]{4,15}$. هذا التعبير النمطي هو نمط قائمة بيضاء (whitelisting) صارم جداً. إنه يطابق كل سلسلة نصية لا تتكون أحرفها إلا من a-z، A-Z، أو 0-9. ليس هذا فقط، بل يجب أن يكون طول المدخل 4 أحرف كحد أدنى و 15 حرفاً كحد أقصى. علامة الكاب (^) في البداية وعلامة الدولار ($) في نهاية التعبير النمطي تشيران إلى أن المطابقة يجب أن تحدث للمدخل بأكمله.
الآن لنفترض أننا نحصل في وقت التشغيل على المدخل التالي الذي لن يجتاز اختبار التعبير النمطي الخاص بنا: o'neal. هل يعني ذلك أن برنامجنا يواجه متسللاً؟ المدخل يبدو بريئاً. ومع ذلك، قد يكون الأمر أيضاً أن مستخدماً خبيثاً يحاول فقط التحقق من وجود ثغرة أمنية لحقن SQL قبل الشروع في الهجوم الفعلي، وهو ما يُعرف أيضاً بالاستطلاع (reconnaissance). على أي حال، لا يزال من الصعب استنتاج أي سوء نية من هذه الحالة بالذات. ومع ذلك، لا يزال بإمكاننا اكتشاف المتسللين باستخدام أشكال أخرى من القوائم البيضاء (whitelists) الفاشلة، مثل محاولات إدخال فاشلة ضد قائمة من القيم الثابتة (hard-coded values) المتوقعة. مثال ممتاز هو معيار رمز الويب JSON (JSON Web Token - JWT). نستخدم JWT عندما نريد من أطراف ثالثة أن ترسل لنا مطالبة (claim) يمكننا التحقق منها ثم الوثوق بالبيانات الموجودة بداخلها. يحتوي المعيار على هيكل JSON بسيط: رأس (header)، وجسم (body)، وتوقيع (signature). يحتوي الرأس (header) على كيفية إنتاج هذه المطالبة (claim) الخاصة وبالتالي التحقق منها. يحتوي الجسم (body) على المطالبة (claim) نفسها. التوقيع (signature) موجود، حسناً، للتحقق. على سبيل المثال، عندما نحصل على الرمز المميز (token) التالي من طرف ثالث، مثل مستخدم، نتحقق منه باستخدام الخوارزمية التي يقدمها في قيمة الرأس (header). في هذه الحالة، يخبرنا الرمز المميز (token) نفسه أنه يجب علينا استخدام خوارزمية التجزئة التشفيرية HMACSHA256 (HS256 في الرمز المميز هي نسخة مختصرة) على بيانات الرأس (header) والجسم (body) لاختبار ما إذا كانت تنتج نفس التوقيع المعطى. إذا أنتجت نفس قيمة التوقيع، فإن الرمز المميز (token) أصيل ويمكننا الوثوق بالجسم (body):
// Header
{
"alg" : "HS256" ,
"typ" : "JWT"
}
// Body
{
"userid" : "johndoe@gmail.com" ,
"name" : "John Doe" ,
"iat" : 1516239022
}
// Signature
AflcxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5g
توجد مكتبات خارجية متنوعة يمكننا استخدامها بسهولة لإنتاج والتحقق من رموز JWT. كان لدى بعضها ثغرة أمنية خطيرة سمحت باعتبار أي JWT رمزاً مميزاً أصيلاً. إليك ما حدث خطأ في تلك المكتبات. ماذا يحدث عندما يحتوي رمز مميز (token) يجب علينا التحقق منه على رأس (header) مثل ما يلي؟ أقدم الرأس هنا فقط، لكنه يحتوي أيضاً على أجزاء الجسم (body) والتوقيع (signature):
// Header
{
"alg" : "None" ,
"typ" : "JWT"
}
يبدو أنه بالنسبة لهذا الرمز المميز (token) المحدد، فإن بعض مكتبات التحقق من JWT تقبل الجسم (body) كما هو دون أي تحقق، لأن None تشير إلى عدم تطبيق أي خوارزمية لإنتاج التوقيع. لوضع هذا في سياقه، فهذا يعني أن أي مستخدم نهائي يمكنه إرسال أي userid إلينا داخل الرمز المميز (token) ولن نطبق أي تحقق ضده ونسمح لهم بتسجيل الدخول. أفضل طريقة لتجنب هذه المشكلة والمشاكل الأمنية المماثلة هي الاحتفاظ بقائمة صالحة من الخوارزميات من جانبنا. في هذه الحالة، قد تحتوي القائمة على خوارزمية صالحة واحدة فقط. علاوة على ذلك، من الأفضل عدم معالجة الخوارزمية التي نحصل عليها داخل جزء الرأس (header) من رمز الويب JSON (JSON Web Token)، مهما كانت. ولكن كما قد تكون خمنت بالفعل، هناك فرصة كبيرة هنا. قد نكتفي بالحصول على قيمة الخوارزمية من جزء الرأس (header) والتحقق منها حتى لو لم نستخدمها. إذا كانت القيمة أي شيء آخر غير ما نتوقعه، لنقل HS256، فهذا يعني أن شخصاً ما يعبث بنا. يمكن استخدام نفس الطريقة لأي قائمة من القيم الثابتة (hard-coded values) المقدمة للمستخدم النهائي والتي نتوقع الحصول على إحداها كمدخل. على سبيل المثال، إذا قدمنا قائمة من المدن في مربع اختيار (select box)، فنحن متأكدون أننا سنستعيد إحداها عند إرسال النموذج. إذا حصلنا على قيمة مختلفة تماماً، فمن المؤكد أن هناك شيئاً خاطئاً في سلوك المستخدم أو الأداة الآلية التي نواجهها.
إجراءات ضد المصادقة (AuthN) والتفويض (AuthZ)
تعد آليات المصادقة (authentication - AuthN) والتفويض (authorization - AuthZ) من أهم أجزاء البرامج من وجهة نظر أمنية. هذه هي الأماكن التي نفرض فيها أن الأطراف التي نعرفها فقط هي من تصل إلى التطبيق وأنها تصل إلى أجزاء معينة ضمن أدوارها. بمعنى آخر، يجب ألا يستخدم مستخدمونا أجزاء معينة من تطبيقنا دون أي تحقق من بيانات الاعتماد، ويجب ألا يصلوا إلى أجزاء لا يملكون فيها أي امتيازات. توجد سيناريوهات هجوم متنوعة ضد كلتا الآليتين، ومع ذلك، فإن الأكثر وضوحاً ضد المصادقة هو هجمات القوة الغاشمة (brute forcing). إنه محاولة مجموعة من بيانات الاعتماد المعبأة مسبقاً أو التي تم إنشاؤها أثناء التنفيذ، واحدة تلو الأخرى، على أمل أن ينجح واحد أو أكثر منها. بالطبع، توجد طرق معروفة لمنع مثل هذه الهجمات: استخدام CAPTCHAs أو تطبيق التقييد (throttling) على عناوين IP أو أسماء المستخدمين التي تسبب مشاكل. عادةً ما تكون هجمات المصادقة معروفة جيداً، وعند ملاحظتها يتم تسجيلها بالفعل وربما تغذيتها في أنظمة مراقبة الأمن. الشيء نفسه ممكن مع الهجمات ضد التفويض. من السهل إنتاج سجل أمني وتنبيه عندما يُرجع تطبيقنا استجابة 403 لمستخدمينا. تعد استجابة HTTP المعروفة هذه مؤشراً على مشكلة في التفويض، لذا من الحكمة تسجيلها. ومع ذلك، فإن حالات المصادقة والتفويض حتى الآن لديها القدرة على إنتاج إنذارات خاطئة. ومع ذلك، ما زلت أشجع على التسجيل وإنتاج الإنذارات كلما حدثت هذه الأمور.
الآن، دعنا نركز على حالة أكثر صلابة. كلما استخدمنا أطر عمل Model-View-Controller (MVC)، فإننا نستفيد من ميزة الربط التلقائي (auto-binding) المضمنة لمعاملات دوال الإجراءات (Action method parameters) الخاصة بنا. لذا، فإن إطار عمل MVC الذي نستخدمه مسؤول عن ربط المعاملات في طلبات HTTP بكائنات النموذج (model objects) الخاصة بنا تلقائياً. هذا يمثل راحة كبيرة لنا، حيث أن الحصول على كل مدخل مستخدم باستخدام واجهات برمجة التطبيقات (APIs) منخفضة المستوى لإطار العمل يصبح مملاً حقاً بعد فترة. ماذا يحدث إذا أصبح هذا الربط التلقائي (auto-binding) متساهلاً للغاية؟ لنفترض أن لدينا نموذج مستخدم (User model). من المحتمل أن يحتوي على ما لا يقل عن عشرة أو عشرين حقلاً عضواً. ولكن للتوضيح، دعنا نقول إنه يحتوي على حقلي العضو FullName و IsAdmin. الحقل العضو الثاني سيشير إلى ما إذا كان مستخدم معين مسؤولاً أم لا:
public class User
{
public string FullName { get ; set ; }
public bool IsAdmin { get ; set ; }
}
لكي يتمكن المستخدمون من تحديث ملفاتهم الشخصية، نقوم بإعداد عرض (View) يتضمن النموذج والروابط المناسبة. أخيراً، عند إرسال النموذج، سيقوم إجراء المتحكم (controller action) بربط معلمات HTTP تلقائياً بكائن من فئة User. ثم، ربما سيتم حفظه في قاعدة البيانات تماماً كما يلي:
[ HttpPost ]
public Result Update ( User user )
{
UserRepository.Store(user);
return View( "Success" );
}
من الواضح هنا، أن مستخدماً خبيثاً غير إداري قد يقوم أيضاً بتعيين قيم لأعضاء نموذج غير مرغوب فيها، مثل IsAdmin. بما أن الربط تلقائي، يمكن لمستخدمنا الخبيث أن يجعل نفسه مسؤولاً عن طريق إرسال طلب HTTP POST بسيط إلى هذا الإجراء! باستخدام نمط MVC، يصبح كل نموذج نستخدمه في معاملات دوال الإجراءات (action method parameters) مرئياً وقابلاً للتحرير بالكامل للمستخدمين النهائيين. أفضل طريقة لمنع ذلك هي استخدام كائنات ViewModels أو DTO إضافية للعروض (Views) والإجراءات (Actions) وتضمين الحقول المسموح بها فقط. على سبيل المثال، إليك UserViewModel الذي يحتوي فقط على الحقول القابلة للتحرير من فئة نموذج المستخدم (User model class).
public class UserViewModel
{
public string FullName { get ; set ; }
}
لذا، فإن المستخدم النهائي، على الرغم من قدرته على إضافة معامل IsAdmin إضافي إلى طلب HTTP POST، لن يتم استخدام تلك القيمة على الإطلاق لتسبب مشكلة أمنية. ممتاز! ولكن انتظر، هناك فرصة ذهبية هنا لاكتشاف المتسللين المتقدمين. ماذا لو قمنا بتضمين خاصية IsAdmin في UserViewModel الخاص بنا، ولكن ننتج سجلاً أمنياً وربما تنبيهات عندما يتم استدعاء الدالة الضابطة (setter):
public class UserViewModel
{
public string FullName { get ; set ; }
public bool IsAdmin
{
set
{
// log, alarm, notify
}
}
}
فقط تأكد من أننا لا نستخدم حقل العضو هذا عندما نقوم بإنشاء كائن من فئة نموذج المستخدم (User model class) من كائن UserViewModel هذا.
متنوعات
من المستحيل سرد أو تصنيف كل حالة ممكنة حيث يمكننا وضع ضوابطنا الصغيرة لملاحظة أي محاولات اختراق في أقرب وقت ممكن. ومع ذلك، إليك بعض الفرص الأخرى المتاحة لنا:
- إذا كان تطبيقنا يوفر تدفقاً من الإجراءات التي يجب اتباعها بترتيب معين، فإن أي ترتيب غير صالح للاستدعاء يشير إلى سلوك غير طبيعي.
- هجمات الحقن (
Injection attacks) هي واحدة من أخطر فئات الثغرات الأمنية التي تنبع من شيفرة غير آمنة ودمج البيانات. هجماتCross Site Scripting (XSS)، وحقنSQL، واختراق الدليل (Directory Traversal) هي بعض الثغرات الشائعة في هذه الفئة. بمجرد استخدامنا لبنى آمنة مثل الترميزات السياقية، والتحقق من القائمة البيضاء (whitelist validation)، والعبارات المعدة (prepared statements)، فإننا نتخلص منها. ومع ذلك، للأسف، لا توجد طرق بسيطة وغير قائمة على القائمة السوداء (non-blacklist) لاكتشاف المتسللين الذين ما زالوا يحاولون استغلال هذه الثغرات الأمنية بمجرد إصلاحها. - إعداد الفخاخ (
traps) هو أيضاً طريقة صالحة لاكتشاف المحاولات الخبيثة، لكنني أعارضها إذا استغرق الجهد وقتاً طويلاً أو كان من المحتمل أن ينتج إنذارات خاطئة. على سبيل المثال، من الممكن تضمين روابط مخفية (display:none) في صفحات الويب الخاصة بنا وتشغيل تسجيل الأمان عندما يتم الوصول إلى هذه الروابط بواسطة الماسحات الأمنية الآلية (لأنها تحاول الوصول إلى كل رابط يمكنها استخراجه). ومع ذلك، قد يؤدي هذا أيضاً إلى إنذارات خاطئة لبرامج الزحف الشرعية، مثلGoogle. ومع ذلك، هذا خيار تصميم وهناك الكثير من الفخاخ التي يمكن إعدادها، مثل أزواج اسم المستخدم وكلمة المرور غير الموجودة ولكن سهلة التخمين، على سبيل المثال:admin:admin؛ مساراتURLالإدارية، على سبيل المثال:/admin؛ رؤوسHTTP، ومعاملاتها، على سبيل المثال:IsAdmin.
الخاتمة
“التسامح ليس الموافقة على ما حدث، بل هو اختيار الارتقاء فوقه.” روبن شارما
من السذاجة التي لا تغتفر أن ندع المحاولات الخبيثة تجاه برامجنا تمر دون ملاحظة بينما لدينا بالفعل الأدوات اللازمة للقيام بخلاف ذلك. التسامح صفة أخلاقية عظيمة، ولكن يجب أن نكون على دراية بالأنشطة الخطرة المحيطة بشيفرتنا. على الرغم من الجوانب الفوضوية لتطوير البرمجيات، فإن تطوير شيفرة آمنة هو مهارة بقاء مهمة في هذا العالم المليء بالمتسللين. علاوة على ذلك، لدينا الفرصة لتحسين هذه المهارة بشكل أكبر من خلال ملاحظة الأنشطة الخبيثة بدقة في شيفرتنا وإنتاج سجلات أمنية وتنبيهات لفرق SOC. إن اتخاذ إجراءات ضد السلوكيات الخبيثة في شيفرتنا، كما قرأت في هذا المقال، يمثل جزءاً من الأخطاء البرمجية التي قد يستغلها المتسللون. أشجعك على الاطلاع على تدريبي عبر الإنترنت “أخطاء برمجية يستغلها المتسللون” لإتقان البقية. إذا وصلت إلى هنا، اشكر الكاتب لتبين له اهتمامك. قل شكراً. تعلم البرمجة مجاناً. ساعد منهج freeCodeCamp مفتوح المصدر أكثر من 40,000 شخص في الحصول على وظائف كمطورين. ابدأ الآن.
الخلاصة التقنية
يؤكد هذا المقال على أن الأمن السيبراني لا يقتصر على منع الثغرات فحسب، بل يمتد ليشمل القدرة على اكتشاف المحاولات الخبيثة والاستجابة لها مبكراً. من خلال تطبيق تقنيات بسيطة مثل التحقق من القيم الفارغة، والمعالجة الموجهة للاستثناءات، وتطبيع المدخلات، واستخدام القوائم البيضاء، ومراقبة آليات المصادقة والتفويض، يمكن للمطورين بناء طبقة دفاع إضافية تساهم في تعزيز مرونة التطبيقات ضد الهجمات. هذه الإجراءات الاستباقية تقلل من وقت الاستجابة وتقلل من المخاطر المحتملة، مما يجعلها ضرورية لأي نظام برمجي حديث.