Firebase: Does the bucket owner have access to all data? - javascript

I am making a To-do Android Application using the Firebase backend. For that, I can use a simple access rule which allows read and write access only when uid == auth.uid. However, as the owner of the bucket, I am currently able to see all the to-dos that any user creates. Clearly, this is unexpected behavior. How do I restrict my access to certain data in my own bucket? Or is there any other alternative?
EDIT: More details. The current security rule is as follows
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if uid == auth.uid;
}
}
}
I am working with react-native and following rnfirebase and used Google Sign-in by this SDK wrapper here. The code to upload an image to Firebase storage follows the tutorial here. Here is the snippet in react-native.
const reference = storage().ref('black-t-shirt-sm.png');
return (
<View>
<Button
onPress={async () => {
// path to existing file on filesystem
const pathToFile = `${utils.FilePath.PICTURES_DIRECTORY}/black-t-shirt-sm.png`;
// uploads file
await reference.putFile(pathToFile);
}}
/>
</View>
);

This makes no sense:
match /{allPaths=**} {
allow read, write: if uid == auth.uid;
}
This allows access to the file if the built-in variable auth.uid matches the context variable uid. But you never define uid anywhere, so the rule always fails. In fact, this should not even save as you have a syntax error by not defining uid anywhere before you use it.
Are you saying that with these rules you have a user who is able to see all files? Can you show an example of that?

Related

Getting firebase storage image 403

I'm using Next.js and am struggling to get firebase storage images to render on screen as they keep coming back as 403 errors.
I upload an image to storage when adding a document to firestore with a url nested in it then use that url to get the image
This is my Next Image tag with recipe.main_image.url == https://storage.googleapis.com/chairy-cooks.appspot.com/recipes/0O3Oycow4lJvyhDbwN0j/main_image/190524-classic-american-cheeseburger-ew-207p.png
And recipe.main_image.name == the name of the photo in firebase storage.
<Image
alt={recipe.main_image.name}
src={recipe.main_image.url}
width={650}
height={650}
/>
The alt tag comes back as the right thing (image name in firebase storage) so I know the image is getting sent back but for some reason I'm unauthorised I've tried multiple fixes including changing firebase rules to
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
AND
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write;
}
}
}
But neither change the outcome I've also set my next config up so I'm getting firebase storage
images: {
domains: ['images.ctfassets.net', 'storage.googleapis.com'],
},
If the images will be served to the public, then i would suggest to grant public access to the image. Cloud Storage for Firebase is backed by Google Cloud Storage. You can make individual objects publicly readable or make the bucket publicly readable.
or better yet, generate signed URLs, this way you can grant limited time access to an object. If you're working with Javascript then you may take a look at this sample code on how to generate a signed URL.
/**
* TODO(developer): Uncomment the following lines before running the sample.
* Note: when creating a signed URL, unless running in a GCP environment,
* a service account must be used for authorization.
*/
// The ID of your GCS bucket
// const bucketName = 'your-unique-bucket-name';
// The full path of your file inside the GCS bucket, e.g. 'yourFile.jpg' or 'folder1/folder2/yourFile.jpg'
// const fileName = 'your-file-name';
// Imports the Google Cloud client library
const {Storage} = require('#google-cloud/storage');
// Creates a client
const storage = new Storage();
async function generateV4ReadSignedUrl() {
// These options will allow temporary read access to the file
const options = {
version: 'v4',
action: 'read',
expires: Date.now() + 15 * 60 * 1000, // 15 minutes
};
// Get a v4 signed URL for reading the file
const [url] = await storage
.bucket(bucketName)
.file(fileName)
.getSignedUrl(options);
console.log('Generated GET signed URL:');
console.log(url);
console.log('You can use this URL with any user agent, for example:');
console.log(`curl '${url}'`);
}
generateV4ReadSignedUrl().catch(console.error);
You may visit this documentation for more information.

Can a user update the script in a console and access other users' data in my firebase code?

For context, I display my Firebase app's configuration in the app.js file (is this safe?) and in order to give a user their data I list the following code:
db.collection('whispers').where("sender", "array-contains",
userID).orderBy('time').onSnapshot(snapshot => {
let changes = snapshot.docChanges();
changes.forEach(change => {
renderSay(change.doc);
});
});
Could a user remove the 'where' condition and access all data in this collection? I only use anonymous sign in so nothing special required in accessing their own data besides using the same device and not clearing data.
I doubt that could be possible by reverse-engineering but the actual security lies in the security rules.
You can change them from the Firebase Console
Here is a video from Firebase and here is the documentation
service cloud.firestore {
match /databases/{database}/documents {
// Match any document in the 'users' collection
match /users/{userID} {
allow read: if request.auth.uid === userID;
allow write: if request.auth.uid === userID;
}
}
}
Using the above security rules will allow users to read and write their data ONLY.
So even if they reverse-engineer your code, it will harm their data only and other data will be safe.
For more details please check the links above and you will get familiar with wildcard [{userID} in this case]
PS: Only you or anyone with access to your Firebase Console can change the rules
EDIT: This Medium Article has many types of rules explained. Please have a read there too :D

Secure authentication structure in a React Webapp

I am currently learning React and worked through some courses but still haven't completely understood how to create a proper structure for a secure web app.
For the sign in, sign up flow I use the firebase SDK. Once logged in, a user gets redirected to a private route. Right now I only have 2 user roles. Guests and signed in Users. This enables me to create private routes by using an inbuild firebase function. This is the first problem as it is not scalable once I add different roles as it would force me to send a request to the backend to check what role the user is and thus which pages he can acces.
if (firebase.auth().currentUser === null) {
console.log("not logged in")
return (<Redirect
to={{
pathname: "/signin",
state: {
from: props.location
}
}}
/>);
}
So I thought that the easiest option would be to use Context, which did work. Once a user loggs in, the server sends a user object which the app refers to for the rest of the session. I followed a bunch of tutorials and they all had the same problem that when using chrome developer tools with the react features, you could just edit the state of the user and bypass the private routes etc.
Second Try:
<UserContext.Consumer>{(context)=>{
const {isLoggedIn} = context
return(
<Route
{...rest}
render={props => {
if (isLoggedIn) {
console.log("not logged in")
return (<Redirect
to={{
pathname: "/signin",
state: {
from: props.location
}
}}
/>);
I'd be grateful if somebody could point me in a direction as it seems like I am missing something important.
EDIT 1: Or is it simply that once you build the app, you can no longer access these states and it's considered safe?
when using chrome developer tools with the react features, you could just edit the state of the user and bypass the private routes
Your routes will never be truly private. They are part of the JavaScript bundle that gets downloaded and rendered by the browser, so they should never contain anything secret. Anyone could read this code if they really wanted to.
Consider this:
if (loggedIn) {
return <div>Secret data: ABC</div>;
}
The string "ABC" is contained in your app build, and is not really a secret anymore. The average user wouldn't know how to obtain it, but a developer probably would, for example by toggling some state in the developer console.
However, the data that comes from Firestore (or any another backend service) should be properly protected. Permission checks are done server-side before this data is sent to the browser. So, unless the user has the required permissions, the data will never be exposed to the wrong person, even if someone tampers with your client-side code in the developer console.
if (loggedIn) {
fetchDataFromBackend();
}
It doesn't matter if someone changes loggedIn to true so that fetchDataFromBackend() is called; the server will make sure the data isn't returned unless the user has the proper permission (e.g. is logged in). In the case of Firebase (Firestore), this protection is achieved with Security Rules.
And, by the way, the recommended way to get the current user with Firebase is to add a listener to the Auth object:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
You could put this in a top-level component and share the user object with child components through a context. That way you don't have to call firebase.auth() all over the place. Here's a good starting point if you need some inspiration: https://usehooks.com/useAuth/
I think what you are doing on the frontend site is good, but you would also need logic in the backend to protect your routes. This means that an user may be able to circumvent your route protection via dev tools on the frontend, but your backend would only send error messages to him, as it recognizes that he has no allowance.
You could do this with Higher Order Functions like this one:
const authenticationWrapper = createResolver(
async ( models, session, SALT ) => {
try {
if (!session) {
throw new Error("No valid credentials!");
}
const { id } = verify(session.token, salt);
const valid = databasecall //
if (!valid) {
throw new Error("No valid user!");
}
return true
} catch (error) {
session.destroy((err) => {
if (err) {
console.error(err);
}
});
}
}
);
All private backend functions would be wrapped into and authentication of the user would be checked every time.
The principle to check in Front- and Backend is called dual authentication. You can read more about it here.

Build private chats in Firebase and Javascript

I am building an app with Firebase that requires private messaging between users.
What I need is to build single chats for 1-1 chats, storing messages in Firestore.
My idea: I guess the best way is to build a single collection for each chat, with the right security rules. Let's say an user with tokenID 1234 wants to talk with user with tokenID 1111, a new collection called 1234_1111 will be created (if not existing), and the security will allow only these two users to read and write.
My question: Is it the right way? And how to do that in Javascript? I'm not sure how to define security rules directly in the JS code, neither how to create a collection with the two users ID.
Security Rules are not defined in your JavaScript code, they are defined separately. What you suggest would be a reasonable approach to take although I'd use a subcollection for it, and a simplified version of your security rules might look something like:
service cloud.firestore {
match /databases/{database}/documents {
match /dms/{idPair}/messages/{msgId} {
allow read, write: if
idPair.split('_')[0] == request.auth.uid ||
idPair.split('_')[1] == request.auth.uid;
}
}
}
Then in your JS code you could do something along the lines of:
// generate idPair
function dmCollection(uid) {
const idPair = [firebase.auth().currentUser.uid, toUid].join('_').sort();
return firebase.firestore().collection('dms').doc(idPair).collection('messages');
}
// send a DM
function sendDM(toUid, messageText) {
return dmCollection(toUid).add({
from: firebase.auth().currentUser.uid,
text: messageText,
sent: firebase.firestore.FieldValue.serverTimestamp(),
});
}
// retrieve DMs
function messagesWith(uid) {
return dmCollection(uid).orderBy('sent', 'desc').get();
}
Note that the idPair is constructed by joining a sorted pair of UIDs so that it will be stable no matter which user sends.

Administrator Views for a Firebase Web Application: How To

My Firebase web app requires administrator access, i.e., the UI should show a few things only for admins (an 'administrator' section). I came up with the below as a means to authorize the UI to display the admin section for valid admins only. My question is, good or bad? Is this a sound means of authorizing? ...so many ways to do this. This particular way requires me to configure admins in the security rules (vs in a node/tree in a db/firestore)
My idea is that if the .get() fails due to unauthorized access, I tell my app logic the user is not an admin, if the .get() succeeds my logic shows the 'admin' sections. Of course, the 'sections' are just HTML skeletons/empty elements populated by the database so even if the end user hacks the JS/logic, no real data will be there - only the empty 'admin section' framework.
function isAdmin(){
return new Promise(function(resolve, reject){
var docRef = firebase.firestore().collection("authorize").doc("admin");
docRef.get().then(function(result) {
if (result) {
resolve (true);
}
}).catch(function(error) {
resolve (false);
});
});
}
The firestore rule specifies the 'admins' by UID.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid == "9mB3UxxxxxxxxxxxxxxxxxxCk1";
}
}
}
You're storing the role of each user in the database, and then looking it up in the client to update its UI. This used to be the idiomatic way for a long time on realtime database, and it still works on Firestore.
The only thing I'd change is to have the rules also read from /authorize/admin, instead of hard-coding the UID in them. That way you only have the UID in one place, instead of having it in both the rules and the document.
But you may also want to consider an alternative: set a custom claim on your admin user, that you can then read in both the server-side security rules (to enforce authorized access) and the front-end (to optimize the UI).
To set a custom claim you use the Firebase Admin SDK. You can do this on a custom server, in Cloud Functions, but in your scenario it may be simpler to just run it from your development machine.
Detailed How To: Firebase has what's called Custom Claims for this functionality as detailed in their Control Access with Custom Claims and Security Rules. Basically, you stand up a separate node server, install the Firebase AdminSDK:
npm install firebase-admin --save
Generate/Download a Private Key from the Service Accounts tab in the Firebase Console and put that on your node server. Then simply create a bare bones node app to assign Custom Claims against each UID (user) that you wish. Something like below worked for me:
var admin = require('firebase-admin');
var serviceAccount = require("./the-key-you-generated-and-downloaded.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://xxxxxxxxxxxxxxxxxxxx.firebaseio.com"
});
admin.auth().setCustomUserClaims("whatever-uid-you-want-to-assign-claim-to", {admin: true}).then(() => {
console.log("Custom Claim Added to UID. You can stop this app now.");
});
That's it. You can now verify if the custom claim is applied by logging out of your app (if you were previously logged in) and logging back in after you update your web app's .onAuthStateChanged method:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
firebase.auth().currentUser.getIdToken()
.then((idToken) => {
// Parse the ID token.
const payload = JSON.parse(window.atob(idToken.split('.')[1]));
// Confirm the user is an Admin.
if (!!payload['admin']) {
//showAdminUI();
console.log("we ARE an admin");
}
else {
console.log("we ARE NOT an admin");
}
})
.catch((error) => {
console.log(error);
});
}
else {
//USER IS NOT SIGNED IN
}
});

Categories