June 28, 2026
السلام عليكم 👋
Critical BAC tips

By Abdlalicc
4 min read
السلام عليكم 👋
لقيت ثغرة أمنية خطيرة في منصة SaaS أوروبية متخصصة في كشوف الرواتب.
الثغرة كانت في إعدادات Firebase Storage — أي مستخدم عادي مسجل في التطبيق كان يقدر يشوف ويحمّل ملفات كل العملاء الآخرين بدون أي قيود. وهذا يشمل:
🔴 تقارير الرواتب الشهرية 🔴 البيانات الشخصية للموظفين 🔴 قسائم الرواتب الفردية بالأرقام الحقيقية 🔴 الإقرارات الضريبية المقدمة للحكومة 🔴 العقود القانونية الموقعة
الأخطر من هذا — إذا فتحت الرابط من المتصفح مباشرة تلقى 403 خطأ، يعني يبدو آمن. لكن باستخدام Burp Suite وإرسال نفس الطلب مع توكن المستخدم، يرجع 200 OK مع ملفات كل العملاء. 🎯
أبلغت الشركة بالثغرة، صلحوها في أقل من 24 ساعة، وأكدوا مكافأة مالية (Bug Bounty). ✅
الكامل في التقرير ادناه 👇👇
How I Found a Critical Firebase Storage Misconfiguration Exposing All Customers' Payroll Data
Introduction
During a routine security assessment of a European SaaS payroll platform, I discovered a critical Broken Access Control vulnerability in their Firebase Storage configuration. The issue allowed any authenticated user — including a free trial account with zero company data — to list and access private files belonging to all other customers on the platform.
This included payroll reports, individual employee payslips, personal data, tax declarations, and signed legal documents. The vulnerability was reported responsibly and fixed within 24 hours.
Reconnaissance
The target was a web application built on React (SPA). The first thing I did was intercept the authentication flow using Burp Suite.
During login, I noticed the app was using Firebase Authentication:
POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSy...POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSy...This told me the entire backend was Firebase-based. I captured the idToken from the response — a signed JWT issued by Google.
Extracting the Firebase Project ID
I base64-decoded the JWT payload and found:
{
"iss": "https://securetoken.google.com/TARGET-PROD",
"aud": "TARGET-PROD"
}{
"iss": "https://securetoken.google.com/TARGET-PROD",
"aud": "TARGET-PROD"
}Firebase Project ID = target-prod
Every Firebase project has a default Storage bucket following this pattern:
target-prod.appspot.comtarget-prod.appspot.comNo guessing required — it's a Google convention.
Testing Firebase Storage Access
Firebase Storage exposes a REST API for file listing:
GET /v0/b/target-prod.appspot.com/o
Host: firebasestorage.googleapis.com
Authorization: Bearer <idToken>GET /v0/b/target-prod.appspot.com/o
Host: firebasestorage.googleapis.com
Authorization: Bearer <idToken>I sent this request using my test account token — an account that had no company data associated with it.
Expected Response:
HTTP/2 403 Forbidden
{ "error": { "code": 403, "message": "Permission denied." } }HTTP/2 403 Forbidden
{ "error": { "code": 403, "message": "Permission denied." } }Actual Response:
HTTP/2 200 OKHTTP/2 200 OK{
"items": [
{ "name": "COMPANY_A/accounting/2024-10-01/payroll_report_october.csv" },
{ "name": "COMPANY_A/documents/employee_personal_data.pdf" },
{ "name": "COMPANY_B/payslips/2025-05-01/individual_payslip_employee123.pdf" },
{ "name": "COMPANY_B/bmf/2024/tax_declaration_2024.xml" },
{ "name": "COMPANY_C/documents/privacy_agreement_signed.pdf" }
],
"nextPageToken": "..."
}{
"items": [
{ "name": "COMPANY_A/accounting/2024-10-01/payroll_report_october.csv" },
{ "name": "COMPANY_A/documents/employee_personal_data.pdf" },
{ "name": "COMPANY_B/payslips/2025-05-01/individual_payslip_employee123.pdf" },
{ "name": "COMPANY_B/bmf/2024/tax_declaration_2024.xml" },
{ "name": "COMPANY_C/documents/privacy_agreement_signed.pdf" }
],
"nextPageToken": "..."
}The response contained 173,772 bytes of file metadata in a single page, with a nextPageToken indicating even more pages existed.
Why the Browser Shows 403 (But the API Returns 200)
This is the key nuance that confused the vendor initially.
If you open the Storage URL directly in a browser without any token:
https://firebasestorage.googleapis.com/v0/b/target-prod.appspot.com/ohttps://firebasestorage.googleapis.com/v0/b/target-prod.appspot.com/oYou get 403 Forbidden — because anonymous (unauthenticated) access is correctly blocked. The vendor checked their Google Cloud Console, saw "Not public", and initially thought there was no misconfiguration.
But these are two completely separate access control layers:
| Layer | What it controls | Status | |---|---|---| | GCS Bucket settings | Anonymous / public access | ✅ Correctly blocked | | Firebase Storage Rules | Authenticated user access | ❌ Misconfigured |
The Firebase Storage Rules were effectively:
// Vulnerable
allow read: if request.auth != null;// Vulnerable
allow read: if request.auth != null;This means: "any logged-in Firebase user can read everything." Since every registered user has a valid token, every user could list the entire bucket.
Impact
| Data Type | Sensitivity | |---|---| | Monthly payroll reports (CSV/PDF) | 🔴 Critical | | Employee personal data (name, address) | 🔴 Critical | | Individual payslips with salary figures | 🔴 Critical | | Tax declarations submitted to authorities | 🔴 Critical | | Signed legal documents | 🟠 High |
Every single customer on the platform was affected. Not one company — all of them.
From a GDPR perspective, payroll data is sensitive personal data. This constituted a personal data breach under Art. 33 GDPR, requiring supervisory authority notification within 72 hours.
Root Cause
Firebase Storage Security Rules are separate from Google Cloud Storage bucket permissions. Many developers correctly set their GCS bucket to "Not public" but forget that Firebase Rules govern authenticated access independently.
The mistake is extremely common — Firebase's default rules in older projects were overly permissive, and many teams never revisited them.
The Fix
The vendor updated their Firebase Storage Rules to:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{companyId}/{allPaths=**} {
allow read, write: if request.auth != null
&& request.auth.uid == companyId;
}
}
}rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{companyId}/{allPaths=**} {
allow read, write: if request.auth != null
&& request.auth.uid == companyId;
}
}
}This ensures each authenticated user can only access files within their own company folder. After the fix, the same request returned:
HTTP/2 403 Forbidden
{ "error": { "code": 403, "message": "Permission denied." } }HTTP/2 403 Forbidden
{ "error": { "code": 403, "message": "Permission denied." } }Fixed within 24 hours of the report. Bug bounty confirmed by the vendor.
Key Takeaways for Developers
- Firebase Rules ≠ GCS Bucket permissions — they are two separate layers. Always check both.
- Never use
allow read: if request.auth != nullas your only rule — it grants access to all authenticated users across your entire bucket. - Scope rules by user/company ID — always validate that the requesting user owns the resource they're accessing.
- Test with a fresh account that has no data — if it can see other users' data, your rules are wrong.
Key Takeaways for Security Researchers
- Always intercept Firebase auth flows — the JWT payload leaks the project ID for free.
- Firebase project ID = storage bucket name —
PROJECT-ID.appspot.comis always the default bucket. - Test the Storage listing endpoint with just a basic user token — it's often overlooked.
- Don't trust the browser — a 403 in the browser doesn't mean the API is secure.
Timeline
| Date | Event | |---|---| | Day 1 | Vulnerability discovered and reported | | Day 1 | Vendor acknowledged and began investigation | | Day 2 | Fix deployed to production | | Day 2 | Fix verified by researcher — 403 confirmed | | Day 2 | Bug bounty confirmed by vendor |
#BugBounty #CyberSecurity #Firebase #ResponsibleDisclosure #InfoSec