تخطي إلى المحتوى الرئيسي

أمان تطبيقات الويب لأي ديفيلوبر: SQL Injection وXSS وCSRF

أمان تطبيقات الويب لأي ديفيلوبر: SQL Injection وXSS وCSRF

أمان تطبيقات الويب مش حاجة بتتضاف في الآخر لما المشروع يخلص، هو قرارات صغيرة بتاخدها وانت بتكتب كل route وكل فورم. وأغلب الثغرات المشهورة بتيجي من غلطة واحدة: إنك صدقت حاجة جاية من المستخدم من غير ما تتأكد منها.

المقال ده بيشرح أمان تطبيقات الويب للمبرمجين من خلال تلات ثغرات هتقابلها في أي مشروع: SQL Injection وXSS وCSRF. هتشوف الكود اللي فيه الثغرة، والكود اللي بيقفلها، وهنراجع كمان ثغرة موجودة في مشروع الميرن ستاك بتاعنا إحنا نفسه.

الخلاصة في أربع نقط:

  • أي مدخل جاي من المستخدم (فورم، رابط، هيدر، JSON) لازم تتعامل معاه على إنه مش موثوق.
  • الـ SQL Injection بتتقفل بالـ parameterized queries، والـ XSS بإنك تعرض النص كنص مش كـ HTML، والـ CSRF بـ SameSite.
  • تسجيل الدخول لوحده مش بيحمي بيانات الناس من بعض، لازم تتأكد إن المستخدم صاحب الحاجة اللي بيعدلها.
  • جرب الهجمات على مشروعك إنت بس، مش على مواقع غيرك.

المحتويات

أولاً: أمان تطبيقات الويب: فكرة واحدة بتفسر أغلب الثغرات

أي خانة في تطبيقك بتستقبل نص هي باب. اللي بيكتب في الخانة ممكن يكون مستخدم عادي، وممكن يكون حد بيجرب يخلي تطبيقك ينفذ حاجة مكتوبة جوه النص نفسه. الثغرات اللي هنشرحها كلها نفس الحكاية: البرنامج بيخلط بين “بيانات” و”أوامر”، فالنص اللي المفروض يتخزن أو يتعرض بيتفسر على إنه كود.

فيه قايمة بتصدرها مؤسسة OWASP لأخطر مخاطر أمان تطبيقات الويب، وبتتحدث كل كام سنة، وبتتحط مرجع في الإرشادات والمقابلات. أغلب اللي هنشرحه النهارده موجود فيها بشكل أو بتاني.

ثانياً: SQL Injection وأخوها في MongoDB

الثغرة بتحصل لما تركب جملة الاستعلام بنفسك بنص جاي من المستخدم. تخيل فورم تسجيل دخول بيدور على الإيميل في قاعدة بيانات SQL.

// BAD: الاستعلام اتركب بنص مباشر
const sql = "SELECT * FROM users WHERE email = '" + email + "'";
db.query(sql);

// لو المستخدم كتب في خانة الإيميل:  ' OR '1'='1
// الاستعلام بيبقى: SELECT * FROM users WHERE email = '' OR '1'='1'
// والشرط ده دايمًا صحيح، فبيرجع كل المستخدمين

// GOOD: parameterized query، القيمة بتتبعت لوحدها
db.query("SELECT * FROM users WHERE email = ?", [email]);

في النسخة التانية قاعدة البيانات بتستلم الجملة وقيمة الإيميل منفصلين، فمهما كان اللي اتكتب هيتعامل معاه كقيمة نصية بس، مش كجزء من الأمر. ده الحل الأساسي، وكل مكتبة قواعد بيانات محترمة بتدعمه. ومتنساش إن الباسورد لازم يتخزن مشفر زي ما شرحنا في مقال الـ JWT.

لو بتستخدم MongoDB زي مشروعنا، فمفيش SQL يتحقن، بس فيه نسخة منها. الـ express.json() بيحول الـ JSON اللي بيوصل لكائن، فلو حد بعت بدل الإيميل كائن فيه عامل استعلام، MongoDB هيفسره كشرط.

// BAD: لو الـ body جه {"email": {"$ne": null}} هيطابق أي مستخدم
const user = await usersCollection.findOne({ email: req.body.email });

// GOOD: اتأكد إن النوع نص قبل ما تستخدمه
if (typeof req.body.email !== "string") {
  return res.status(400).json({ error: "بيانات غير صالحة" });
}
const user = await usersCollection.findOne({ email: req.body.email });

في route تسجيل الدخول اللي بنيناه، الباسورد لسه بيتفحص بعدها بـ bcrypt فالهجمة دي لوحدها مش هتفتحه. بس نفس النمط في أي route بيدور بحقل واحد (استرجاع حساب مثلاً) ممكن يتستغل. ولو عايز طريقة منظمة، التحقق من شكل البيانات بمكتبة زي Joi اللي ذكرناها في مقال الـ REST API بتغطي الموضوع ده لكل الـ routes مرة واحدة. ولو لسه بتبدأ مع MongoDB، فيه مقال الأساسيات.

ثالثاً: XSS: لما الصفحة تنفذ كود مش بتاعك

الـ XSS (Cross-Site Scripting) هو إن حد يقدر يزرع كود JavaScript في صفحتك، فيتنفذ في متصفح أي زائر. وله ثلاث صور معروفة:

  • Stored: الكود بيتخزن عندك (تعليق مثلاً) وبيتنفذ عند كل واحد يفتح الصفحة.
  • Reflected: الكود بيجي في رابط، والسيرفر بيرجعه في الرد، فيتنفذ لمن يدوس على الرابط.
  • DOM-based: الغلط في كود JavaScript نفسه اللي بيحط مدخل المستخدم في الصفحة.
// BAD: النص بيتفسر كـ HTML
commentBox.innerHTML = comment.text;
// لو comment.text فيه: <img src=x onerror=alert(1)> الكود هيتنفذ

// GOOD: النص بيتعامل كنص عادي
commentBox.textContent = comment.text;

الـ alert هنا مجرد دليل إن الكود اتنفذ. في هجمة حقيقية بيبقى مكانه كود يسرق بيانات أو يبعت طلبات باسم المستخدم. الحل نفسه بسيط: اعرض مدخل المستخدم كنص مش كـ HTML.

الخبر الحلو إن React بيعمل escape تلقائي لأي حاجة بتحطها جوه {} في الـ JSX. الخطر بيرجع لما تستخدم dangerouslySetInnerHTML (والاسم نفسه تحذير). لو فعلاً محتاج تعرض HTML جاي من مستخدم، نضّفه الأول بمكتبة زي DOMPurify.

وفيه نقطة تخص مشروعنا بالذات: في مقال React وفي مقال الـ JWT خزنا التوكن في localStorage، وأي سكريبت بيتنفذ في الصفحة يقدر يقراه. يعني ثغرة XSS واحدة كفاية تسرق توكن المستخدم. ده مش خطأ في الشرح، ده مقايضة معروفة، وبديلها كوكي httpOnly اللي في القسم الجاي.

رابعاً: CSRF والكوكيز

المتصفح بيبعت الكوكيز الخاصة بموقعك تلقائيًا مع أي طلب رايح لموقعك، حتى لو الطلب ده طالع من صفحة موقع تاني. الـ CSRF (Cross-Site Request Forgery) بيستغل ده: موقع خبيث فيه فورم مخفي بيبعت طلب لتطبيقك، والمتصفح بيلزق عليه كوكي الجلسة بتاعتك، فالسيرفر يشوفه طلب شرعي من مستخدم مسجل دخول.

الحماية الأساسية في إعدادات الكوكي نفسه:

res.cookie("token", token, {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
  maxAge: 7 * 24 * 60 * 60 * 1000
});
  • httpOnly: كود JavaScript في الصفحة مش هيقدر يقرا الكوكي، فبيصعّب سرقة التوكن بـ XSS.
  • secure: الكوكي مش بيتبعت غير على HTTPS.
  • sameSite: المتصفح مبيبعتش الكوكي مع الطلبات القادمة من مواقع تانية في أغلب الحالات، وده قلب الحماية من CSRF.

المتصفحات الحديثة بتعتبر Lax هي القيمة الافتراضية في أغلبها، بس اكتبها صراحة بدل ما تعتمد على افتراض. وللتفاصيل الكاملة عن الكوكيز، شرح MDN مرجع موثوق.

ولو بتبعت التوكن في هيدر Authorization زي ما عملنا في مقال الـ JWT، فالمتصفح مش بيحطه لوحده في الطلبات، يعني مفيش CSRF بالشكل ده. لكن لاحظ المقايضة: الهيدر محتاج كود يقرا التوكن من localStorage (معرض لـ XSS)، والكوكي httpOnly بيحمي من ده بس بيحتاج حماية CSRF. مفيش اختيار مجاني، المهم تختار وانت عارف تمنه.

خامساً: “مسجل دخول” مش معناها “صاحب الحساب”

ده أخطر درس في المقال لأنه موجود في مشروعنا. الـ authMiddleware اللي كتبناه في مقال الـ JWT بيتأكد إن الطلب جاي من مستخدم مسجل دخول، بس مش بيتأكد إن المستخدم ده هو صاحب الحساب اللي بيحاول يحذفه. يعني أي مستخدم مسجل يقدر يبعت DELETE على id أي حد تاني وينجح. الثغرة دي اسمها Broken Access Control، وهي من أكتر الثغرات انتشارًا.

app.delete("/users/:id", authMiddleware, async (req, res) => {
  // req.userId جاي من التوكن اللي اتحقق منه
  if (req.userId !== req.params.id) {
    return res.status(403).json({ error: "مش مسموحلك تحذف حساب غيرك" });
  }
  // باقي كود الحذف زي ما هو
});

نفس الفكرة في الـ PUT. وفيه مشكلة تانية قريبة منها في route التحديث اللي في مقال الـ REST API: بنكتب { $set: req.body }، يعني أي حقل يتبعت هيتحفظ. لو ضفت حقل زي role بعدين، أي مستخدم يقدر يبعته ويغير صلاحياته. الأحسن تحدد الحقول المسموحة بالاسم:

const { name, email } = req.body;
await usersCollection.updateOne(
  { _id: new ObjectId(req.params.id) },
  { $set: { name, email } }
);

سادساً: طبقة حماية سريعة بخمس خطوات

الخطوات دي مش بتغني عن اللي فات، لكنها بتقفل مساحة كبيرة بتعب قليل.

const helmet = require("helmet");
const rateLimit = require("express-rate-limit");

app.use(helmet());
app.use("/login", rateLimit({ windowMs: 15 * 60 * 1000, max: 10 }));
  1. helmet: بيضيف مجموعة هيدرز أمان افتراضية لكل رد بسطر واحد.
  2. Rate limiting: بيحدد عدد المحاولات المسموحة، فبيصعّب تخمين الباسورد بالتجربة المتكررة. اسم الخيار max ممكن يتغير لـ limit حسب نسخة المكتبة، فراجع التوثيق.
  3. HTTPS: تشفير للاتصال بشهادة، وشرحنا ده في مقال Nginx وفي مقال أساسيات الشبكات.
  4. الأسرار: JWT_SECRET وبيانات الاتصال بقاعدة البيانات في ملف .env ومش في المستودع، والـ .gitignore هو اللي بيمنع رفعه، زي ما اتشرح في مقال استخدام GitHub.
  5. npm audit: بيفحص المكتبات اللي بتستخدمها بحثًا عن ثغرات معروفة. شغله كل فترة وحدّث اللي فيه مشاكل.

سابعاً: الملخص في جدول

الجدول ده بيجمع اللي فات في مكان واحد، تقدر ترجعله لما تراجع كود جديد.

الثغرةبتحصل لماالعلاج الأساسي
SQL Injectionتركيب الاستعلام بنص جاي من المستخدماستخدم parameterized queries، وتحقق من نوع البيانات
XSSعرض مدخل المستخدم كـ HTMLاعرضه بـ textContent أو escape، ونضّفه بـ DOMPurify لو لازم HTML
CSRFكوكيز بتتبعت تلقائيًا مع طلبات جاية من مواقع تانيةsameSite وhttpOnly وsecure
Broken Access Controlالتأكد إنك مسجل دخول بس، من غير فحص الملكيةفحص الصلاحية في كل route، وتحديد الحقول المسموحة بالاسم
Brute forceمفيش حد لعدد محاولات تسجيل الدخولrate limiting على /login

ثامناً: أخطاء شائعة هتقابلك

  • الاعتماد على التحقق في الفرونت إند بس. أي حد يقدر يبعت طلب بـ curl ويتخطى الفورم بالكامل، فالتحقق الحقيقي لازم يتكرر على السيرفر.
  • تركيب استعلامات بنصوص مضافة بدل parameterized queries.
  • عرض رسايل خطأ تفصيلية أو stack trace للمستخدم، لأنها بتدي المهاجم معلومات عن تطبيقك. سجلها عندك في الـ logs زي ما شرحنا في مقال المراقبة، وارجع للمستخدم رسالة عامة.
  • كتابة الأسرار في الكود أو رفعها على GitHub بالغلط.
  • الظن إن HTTPS لوحده بيحميك. هو بيشفر الطريق بس، ومش بيمنع SQL Injection ولا XSS.
  • تأجيل الأمان لـ “بعد الإطلاق”، ساعتها بيبقى تعديله أصعب وأغلى.

تاسعاً: قائمة فحص قبل النشر

  • كل مدخل جاي من مستخدم بيتفحص نوعه وشكله على السيرفر.
  • كل استعلام قاعدة بيانات parameterized.
  • مفيش innerHTML ولا dangerouslySetInnerHTML بمدخل مستخدم من غير تنضيف.
  • كوكي التوكن (لو بتستخدمه) فيه httpOnly وsecure وsameSite.
  • كل route بيعدل أو بيحذف بيتأكد إن المستخدم صاحب الحاجة، وبيحدد الحقول المسموحة بالاسم.
  • helmet وrate limiting شغالين، وnpm audit اتشغل.
  • ملف .env مش في المستودع.

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


اكتشاف المزيد من كود التطور

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

saeed aly

saeed aly

كاتب ومطور تقني في منصة كود التطور، يشارك المعرفة البرمجية لإثراء المحتوى العربي.

اترك رد