لماذا يحدث هذا: الكود يعمل، لكنه يثق بالجميع
أدوات بناء التطبيقات بالذكاء الاصطناعي تُقاس بسؤال واحد: هل ظهرت الميزة وعملت؟ والطريق الأقصر إلى «تعمل» غالبًا يتجاوز الحماية: استدعاء الخدمة من المتصفح مباشرة بدل الخادم، وفتح الجدول للقراءة والكتابة حتى لا يظهر خطأ صلاحيات، وقبول هوية المستخدم من جسم الطلب لأن ذلك أسهل من التحقق من الجلسة. كل هذا ينجح في المعاينة، ثم يصبح ثغرة يوم تنشر.
الخبر الجيد أن الأخطاء متكررة ومحدودة، فتستطيع فحصها بنفسك حتى لو لم تكن مبرمجًا. خذ مثالًا: نظام حجوزات لعيادة أسنان في الدمام بُني في ساعة. إن كان جدول المواعيد مفتوحًا، يستطيع أي شخص يعرف رابط قاعدة البيانات أن يقرأ أسماء المرضى وجوالاتهم. هذه ليست مشكلة نظرية، بل بيانات شخصية يحميها نظام حماية البيانات الشخصية في المملكة، والتفاصيل في دليل الخصوصية و PDPL.
ما الذي تحتاج أن تبحث عنه: خمس ثغرات متكررة
| الثغرة | كيف تكتشفها | الإصلاح |
|---|---|---|
| مفتاح سري في الواجهة | ابحث في ملفات الواجهة عن sk_ أو secret أو service_role | انقل الاستدعاء إلى الخادم واقرأ المفتاح من متغيّر بيئة |
| جدول Supabase بلا RLS | لوحة Supabase تُظهر الجدول بلا سياسات أو RLS معطّل | فعّل RLS واكتب سياسة لكل عملية |
| مسار API بلا تحقق | افتح المسار بدون تسجيل دخول أو بحساب مستخدم آخر | تحقق من الجلسة واجلب userId منها لا من الطلب |
| لا تحديد لمعدل الطلبات | أرسل 100 طلب تسجيل دخول متتالي، هل تُقبل كلها؟ | Rate limit على تسجيل الدخول والنماذج والمسارات المكلفة |
| حقن SQL أو XSS | ابحث عن استعلامات تُبنى بدمج النص، أو innerHTML مع مدخلات المستخدم | استعلامات بمعاملات، و textContent بدل innerHTML |
مفتاح anon في Supabase مصمم ليظهر في الواجهة، وأمانه كله معتمد على سياسات RLS. أما مفتاح service_role فيتجاوز RLS بالكامل، ووجوده في كود المتصفح يعني أن قاعدة بياناتك مفتوحة لأي زائر.
الحل خطوة بخطوة
افحص المفاتيح في الواجهة والمستودع
شغّل بحثًا سريعًا في مجلدات الواجهة وسجل git. إن وجدت مفتاحًا سريًا سبق رفعه، فلا يكفي حذفه: ألغِه من لوحة الخدمة وأصدر مفتاحًا جديدًا.
code grep -rnE "sk_(live|test)_|service_role|SECRET|PRIVATE_KEY" src/ public/ --include=*.{js,ts,tsx,jsx,html} git log -p | grep -nE "sk_live_|service_role" | headفعّل RLS على كل جدول فيه بيانات مستخدمين
سياسة لكل عملية، تربط الصف بصاحبه عبر
auth.uid(). ابدأ بالمنع ثم افتح ما تحتاجه فقط.تحقق من الهوية في كل مسار API
المسار يقرأ المستخدم من الجلسة أو الرمز المتحقَّق منه، ثم يتأكد أن السجل المطلوب يخصه. لا تقبل
userIdمن جسم الطلب.حدّد معدل الطلبات
على تسجيل الدخول، وإرسال رمز التحقق، ونماذج التواصل، وأي مسار يستدعي خدمة مدفوعة مثل الذكاء الاصطناعي أو الرسائل النصية.
عقّم المدخلات والمخرجات
استعلامات بمعاملات دائمًا، وعرض نص المستخدم كنص لا كـ HTML، وتحقق من نوع وطول كل حقل في الخادم.
alter table public.appointments enable row level security;
create policy "patients read own appointments"
on public.appointments for select
to authenticated
using (patient_id = (select auth.uid()));
create policy "patients create own appointments"
on public.appointments for insert
to authenticated
with check (patient_id = (select auth.uid()));
create policy "patients cancel own future appointments"
on public.appointments for update
to authenticated
using (patient_id = (select auth.uid()) and starts_at > now())
with check (patient_id = (select auth.uid()));
-- No delete policy: deletes are refused for everyone except the service role.// npm i express-rate-limit
import express from "express";
import rateLimit from "express-rate-limit";
const app = express();
app.set("trust proxy", 1); // behind a reverse proxy: use the real client IP
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 10, // 10 attempts per IP per window
standardHeaders: "draft-7",
legacyHeaders: false,
message: { error: "محاولات كثيرة، حاول بعد قليل" },
});
const apiLimiter = rateLimit({ windowMs: 60 * 1000, limit: 120 });
app.use("/api/", apiLimiter);
app.post("/api/login", loginLimiter, express.json(), loginHandler);// Bad: trusts the client
// app.get("/api/orders/:id", (req, res) => db.query(`SELECT * FROM orders WHERE id = ${req.params.id}`))
app.get("/api/orders/:id", requireSession, async (req, res) => {
const id = Number.parseInt(req.params.id, 10);
if (!Number.isInteger(id)) return res.status(400).end();
const order = await db.get(
"SELECT id, total, status FROM orders WHERE id = ? AND user_id = ?",
[id, req.session.userId] // parameters, and the owner comes from the session
);
if (!order) return res.status(404).end();
res.json(order);
});// Bad: el.innerHTML = review.text;
const el = document.createElement("p");
el.textContent = review.text; // rendered as text, scripts never run
list.append(el);راجع المشروع أمنيًا قبل النشر: ابحث عن أي مفتاح سري في ملفات الواجهة وانقله إلى الخادم كمتغيّر بيئة، وتأكد أن كل مسار API يتحقق من الجلسة ويقرأ المستخدم منها لا من الطلب، وأضف تحديد معدل الطلبات على تسجيل الدخول والنماذج، واستبدل أي استعلام مبني بدمج النص باستعلام بمعاملات، وأي innerHTML لمدخلات المستخدم بـ textContent. اكتب لي قائمة بما وجدته وما أصلحته.
قبل النشر وبعده: روتين أمان بسيط
الأمان ليس مراجعة تُعمل مرة واحدة ثم تُنسى. كل ميزة جديدة تضيفها بالمحادثة قد تفتح مسارًا جديدًا أو جدولًا جديدًا، لذلك اجعل هذه العادات جزءًا من طريقة عملك، حتى في مشروع صغير مثل متجر عطور أو نظام حجوزات لصالون.
- بعد كل ميزة تضيف جدولًا: تأكد أن RLS أو التحقق من الملكية مفعّل عليه قبل أن تنتقل للميزة التالية.
- بعد كل ميزة تضيف مسار API: افتحه من نافذة خاصة بدون تسجيل دخول، ثم بحساب مستخدم آخر، ويجب أن يُرفض في الحالتين.
- كل شهر: راجع من لديه صلاحية الوصول إلى المشروع ولوحات الخدمات، واحذف من لم يعد يعمل معك.
- عند أي شك في تسريب: ألغِ المفتاح وأصدر بديلًا أولًا، ثم ابحث عن السبب.
- في السجلات: سجّل محاولات الدخول الفاشلة والطلبات المرفوضة بتحديد المعدل، فهي أول إشارة إلى من يجرّب على تطبيقك.
الحل الأسرع: أوترا
في أوترا يعمل كل مشروع في بيئة معزولة خاصة به، والمشروع خاص افتراضيًا لا يصل إليه إلا من تدعوه من «المشاركة»، مع العلم أن العضو المدعو يحصل على وصول كامل يشمل الأسرار، فادعُ من تثق به فقط. وحين يحتاج مشروعك مفتاحًا، تلصقه في نموذج مخفي الأحرف داخل المحادثة أو في «البيانات السرية»، فيُحفظ كمتغيّر بيئة ولا يظهر نصّه في المحادثة أو في الكود.
وتستطيع أن تبني الخادم والتحقق من الجلسة وتحديد المعدل في المشروع نفسه، ثم تشغّل مراجعة بوضع Review قبل النشر. هذه الأدوات تقلل الأخطاء الشائعة لكنها لا تُغني عن مراجعتك، خاصة في التطبيقات التي تحفظ بيانات صحية أو مالية.
بوضع Review، افحص مشروعي بحسب هذه القائمة: مفاتيح في الواجهة، مسارات API بلا تحقق من المستخدم، غياب تحديد معدل الطلبات، استعلامات قابلة للحقن، وعرض مدخلات المستخدم كـ HTML. أصلح ما تجده واكتب ملخصًا قصيرًا بالعربية.