To add/update/delete a record in Firestore, I can either run code within my app that executes this operation or I can call a cloud function that will handle the operation. Is it "safer" for my app to do it the cloud function way? It takes a bit longer this way, but I worry about Firebase being vulnerable somehow. I once read that to make a really safe app, you should assume that some malicious user has already reverse engineered your app. Should I assume that using Cloud Functions for all changes to Firestore is the safest route or is there no difference?
Using code from the client:
CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> addUser() {
// Call the user's CollectionReference to add a new user
return users
.add({
'full_name': fullName, // John Doe
'company': company, // Stokes and Sons
'age': age // 42
})
.then((value) => print("User Added"))
.catchError((error) => print("Failed to add user: $error"));
}
Using Cloud Functions:
exports.createUser = functions.https.onCall(async (data, context) => {
const uid = context?.auth?.uid
if (!uid) {
return {result: 0, message: 'You are not authorized.'}
}
const username = data.user
const email = data.email
var result: number
var message: string
//Check to see if that username already exists
const doesUserExist = await TS_doesUserExist(username)
const createUser = await TS_createUser(uid, username, email)
if (doesUserExist[0] == 0) {
if (createUser[0] == 1) {
result = 1
message = ''
} else {
result = 0
message = createUser[1]
}
} else {
result = 0
message = doesUserExist[1]
}
return {result: result, message: message}
})
async function TS_createUser(uid: string, username: string, email: string): Promise<[number, string]> {
var result: number = 0
var message: string = ''
//Create SECURE_userinfo doc
const userInfoDoc = await admin.firestore()
.collection(usersCollPath)
.doc(uid) // use uid as document ID
.collection(secureUserInfoCollPath)
.doc()
.set({
createDate: admin.firestore.FieldValue.serverTimestamp(), //signupdate
email: email,
})
//Create stat doc
const statDoc = await admin.firestore()
.collection(usersCollPath)
.doc(uid)
.collection(statCollPath)
.doc()
.set({
stat: 1,
})
//Create username doc
const usernameDoc = await admin.firestore()
.collection(usersCollPath)
.doc(uid)
.collection(usernameCollPath)
.doc()
.set({
username: username,
})
if (userInfoDoc) {
if (statDoc) {
if (usernameDoc) {
result = 1
message = ''
} else {
result = 0
message = 'Could not create username doc'
}
} else {
result = 0
message = 'Could not create status doc'
}
} else {
result = 0
message = 'Could not create user info doc'
}
return [result, message]
}
2
Answers
It depends on what you mean by "safe". Putting your database logic behind a function just makes an attacker use the function for abuse rather than the database interface. They are both perfectly usable attack vectors.
The main difference between direct database access (with security rules) vs cloud functions is that you have more expressive power on what inputs to check in the function, since you have a full power of JavaScript at your disposal. This gives you more options than security rules, but is not necessarily "safer" in any case.
I agree with the answer from Doug Stevenson. It also depends on how often you are calling the operations in your app and how important speed is to you or your users.
I have an established Flutter app that mostly calls Firestore directly using Flutterfire but in some cases calls a cloud function. There is a noticeable delay when calling a cloud function for the first time if it is has not been recently called by the same user or another user.
I would recommend using direct Firestore access if:
As Doug noted, both direct access and access via a cloud function still present vulnerabilities that need to be managed.