Kodax

قواعد الأمان في Firebase — Firebase

قواعد الأمان في Firebase

لأن التطبيق يتصل بـ Firestore و Storage مباشرة من جهاز المستخدم، فقواعد الأمان هي خادمك الوحيد: كل قراءة وكتابة تُقيَّم مقابلها. قواعد خاطئة = بيانات كل المستخدمين مكشوفة أو قابلة للتعديل.

البنية

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /posts/{postId} {
      allow read: if <شرط>;
      allow create, update, delete: if <شرط>;
    }
  }
}

- الرفض افتراضي: ما لم تسمح به قاعدة صراحةً فهو مرفوض.

- read = get + list، و write = create + update + delete — افصلها لتحكم أدق.

- القواعد ليست فلاتر: استعلام قد يعيد وثيقة غير مسموحة يُرفض بالكامل. يجب أن يتضمن الاستعلام نفس الشرط (where("authorId", "==", uid)).

المتغيرات المتاحة

| المتغير | المعنى |

|---|---|

| request.auth | المستخدم (null إن لم يكن مسجلاً)؛ request.auth.uid و request.auth.token |

| resource.data | الوثيقة الحالية في قاعدة البيانات |

| request.resource.data | الوثيقة بعد الكتابة المطلوبة |

الأنماط الأساسية

// الملكية
allow read, update, delete: if request.auth != null && request.auth.uid == resource.data.authorId;

// الإنشاء باسم نفسك فقط
allow create: if request.auth != null && request.resource.data.authorId == request.auth.uid;

// التحقق من البيانات
allow create: if request.resource.data.title is string
              && request.resource.data.title.size() > 0
              && request.resource.data.title.size() <= 100
              && request.resource.data.keys().hasOnly(['title', 'body', 'authorId', 'createdAt']);

تصعيد الصلاحيات: أخطر الأخطاء

القواعد السابقة في هذا الدرس كانت تسمح بـ:

- إنشاء وثيقة أي مستخدم (allow create: if isAuthenticated() على users/{userId}) — فينشئ مهاجم ملفاً باسم غيره.

- تعديل الحقول الحساسة في وثيقته: مستخدم يضيف role: "admin" لنفسه.

- نقل ملكية منشور بتغيير authorId عند التحديث.

الإصلاح في المثال الكامل: الإنشاء للمالك فقط، ومنع الحقول الحساسة بـ diff(...).affectedKeys().hasAny([...])، والتأكد أن authorId لا يتغير. والأدوار تُحفظ في Custom Claims (request.auth.token.admin == true) لا في وثيقة يعدّلها المستخدم.

Storage

نفس الفكرة مع request.resource.size و request.resource.contentType. ضع حداً للحجم والنوع حتى في المسارات "العامة".

اختبر قواعدك

- محاكي Firebase (firebase emulators:start) ومكتبة @firebase/rules-unit-testing: اكتب اختبارات "يجب أن يُرفض" لكل محاولة مهاجم.

- Rules Playground في لوحة التحكم لتجربة طلب محدد.

- لا تنشر allow read, write: if true أبداً، ولا تترك "وضع الاختبار" (الذي يفتح كل شيء لمدة 30 يوماً) في مشروع حقيقي.

تمرين

اكتب اختبارات بالمحاكي تتأكد أن: المستخدم أ لا يقرأ وثيقة المستخدم ب، ولا يستطيع إضافة role لنفسه، ولا يستطيع تغيير authorId لمنشوره، ولا رفع ملف 10MB.

// ===== قواعد Firestore =====
// firebase.rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    
    // دالة مساعدة: هل المستخدم مسجل؟
    function isAuthenticated() {
      return request.auth != null;
    }
    
    // دالة مساعدة: هل المستخدم هو المالك؟
    function isOwner(userId) {
      return request.auth.uid == userId;
    }
    
    // ===== قواعد المستخدمين =====
    match /users/{userId} {
      // القراءة: للمالك فقط
      allow read: if isOwner(userId);
      
      // الإنشاء: المستخدم ينشئ وثيقته هو فقط، دون منح نفسه دوراً
      allow create: if isOwner(userId) && !("role" in request.resource.data);
      
      // التحديث: للمالك فقط، ولا يستطيع تغيير دوره
      allow update: if isOwner(userId)
                    && !request.resource.data.diff(resource.data).affectedKeys().hasAny(["role"]);
      
      // الحذف: ممنوع
      allow delete: if false;
    }
    
    // ===== قواعد المنشورات =====
    match /posts/{postId} {
      // القراءة: للجميع إذا منشور عام
      allow read: if resource.data.public == true 
                    || isOwner(resource.data.authorId);
      
      // الإنشاء: للمسجلين مع بيانات صحيحة
      allow create: if isAuthenticated()
                    && request.resource.data.authorId == request.auth.uid
                    && request.resource.data.title.size() > 0
                    && request.resource.data.title.size() <= 100;
      
      // التحديث: للمالك فقط، ولا يُنقل المنشور لمؤلف آخر
      allow update: if isOwner(resource.data.authorId)
                    && request.resource.data.authorId == resource.data.authorId;
      
      // الحذف: للمالك فقط
      allow delete: if isOwner(resource.data.authorId);
    }
  }
}

// ===== قواعد Storage =====
// storage.rules

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    
    // ملفات المستخدمين
    match /users/{userId}/{allPaths=**} {
      // القراءة: للجميع
      allow read;
      
      // الكتابة: للمالك فقط
      allow write: if request.auth.uid == userId
                   && request.resource.size < 5 * 1024 * 1024  // 5MB max
                   && request.resource.contentType.matches('image/.*');
    }
    
    // ملفات عامة
    match /public/{allPaths=**} {
      allow read;
      // حتى الملفات العامة: حجم ونوع محددان
      allow write: if request.auth != null
                   && request.resource.size < 2 * 1024 * 1024
                   && request.resource.contentType.matches("image/.*");
    }
  }
}
اختبر فهمك