You’ve integrated Firebase Storage. Image upload works. Then you discover someone uploaded a 2 GB file to your public bucket. Or that your security rules allow anyone to read every user photo. It happens more often than you think.
Here at Meteora Web, we’ve seen projects where Firebase Storage rules were still the default — "allow all" — because "the frontend is secure enough". The frontend is never secure enough. A public bucket without controls is a ticking bill. In this hands-on guide we’ll configure secure image upload and Firebase Storage rules, real code included.
Why is Firebase Storage security a real problem?
Firebase Storage runs on Google Cloud Storage. Every file gets a unique URL. If rules are misconfigured, anyone with that URL can download it. Worse: without upload limits, a malicious user can fill your bucket and spike your costs.
Common mistakes we see:
- Open read/write rules for anonymous users
- No file type or size validation at the rule level
- Uploading directly from the frontend without an auth token
- Files never cleaned after user account deletion
These aren’t abstract scenarios. An e-commerce client had product images with public read rules. A competitor scraped the entire image catalog in 10 minutes – zero cost for them, thousands in bandwidth for the client. We always think in terms of cost and risk: a wrong security rule is a hidden cost.
Sponsored Protocol
How to configure security rules for Firebase Storage?
Rules are written in the Firebase console, under Storage -> Rules, in a declarative syntax. Here are the three essential patterns.
1. Public read, authenticated write
For profile pictures, public previews. Anyone can read, only logged-in users can upload.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read: if true;
allow write: if request.auth != null;
}
}
}
2. User-scoped folders
Each user can only read/write their own folder. Perfect for private profiles or documents.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/{allPaths=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
3. File type and size validation
Prevent executable uploads or huge files by checking request.resource.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /images/{imageId} {
allow write: if request.auth != null
&& request.resource.size < 5 * 1024 * 1024 // 5 MB
&& request.resource.contentType.matches('image/.*');
allow read: if true;
}
}
}
Important: request.resource only exists during write operations. This server-side validation is your first defense — it works even if the client is compromised.
Sponsored Protocol
How to implement image upload from web or mobile?
We use the Firebase v9 modular SDK. Below is an upload snippet with progress feedback and custom metadata.
Upload with metadata and progress
import { getStorage, ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage';
import { getAuth } from 'firebase/auth';
const storage = getStorage();
const auth = getAuth();
function uploadImage(file) {
const user = auth.currentUser;
if (!user) throw new Error('User not authenticated');
const storageRef = ref(storage, `users/${user.uid}/images/${Date.now()}_${file.name}`);
const metadata = {
contentType: file.type,
customMetadata: {
'uploadedBy': user.uid,
'originalName': file.name
}
};
const uploadTask = uploadBytesResumable(storageRef, file, metadata);
uploadTask.on('state_changed',
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log('Upload progress:', progress);
},
(error) => {
console.error('Upload error:', error);
},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
console.log('File available at:', downloadURL);
// Store URL in Firestore or elsewhere
});
}
);
}
Why this approach? The path includes the user UID, matching the rule from section 2. Custom metadata enables audit trails. Progress avoids poor UX. Error handling catches auth failures (403).
Sponsored Protocol
Mobile upload (Flutter example)
import 'package:firebase_storage/firebase_storage.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:io';
Future uploadImage(File file) async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) throw Exception('Not authenticated');
final ref = FirebaseStorage.instance
.ref('users/${user.uid}/images/${DateTime.now().millisecondsSinceEpoch}_${file.path.split('/').last}');
final snapshot = await ref.putFile(file);
return await snapshot.ref.getDownloadURL();
}
What steps to take for storage and cost optimization?
Upload is just the beginning. Without size and quality controls, your bucket grows and costs rise. Here are our best practices.
Sponsored Protocol
Client-side compression and resizing
Before uploading, reduce the image on the device. On web, use canvas to resize. On mobile, use libraries like image_picker with maxWidth option. We cut image weight by 60% for one client with zero visual loss, simply by resizing to 1200px on the longest side.
Generate thumbnails with Cloud Functions
Trigger a Cloud Function on object finalization to create a smaller version. Example with Sharp on Node.js:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const sharp = require('sharp');
admin.initializeApp();
exports.generateThumbnail = functions.storage.object().onFinalize(async (object) => {
const bucket = admin.storage().bucket(object.bucket);
const filePath = object.name;
const fileName = filePath.split('/').pop();
const thumbPrefix = 'thumb_';
if (fileName.startsWith(thumbPrefix)) return null;
const thumbPath = filePath.replace(fileName, `${thumbPrefix}${fileName}`);
const downloadedFile = bucket.file(filePath);
const tempLocalPath = `/tmp/${fileName}`;
const thumbLocalPath = `/tmp/${thumbPrefix}${fileName}`;
await downloadedFile.download({destination: tempLocalPath});
await sharp(tempLocalPath).resize(200, 200).toFile(thumbLocalPath);
await bucket.upload(thumbLocalPath, {destination: thumbPath});
});
Retention policies and automatic cleanup
Set GCS Lifecycle Management to delete files not read after 90 days. Or, when a user is deleted, trigger a Cloud Function to remove their Storage folder. We always do this — one client forgot to clean up and storage costs silently accumulated.
Sponsored Protocol
What to do now
Three concrete actions for your Firebase project today:
- Check your current rules in Firebase console > Storage > Rules. If you have
allow read, write: if true;in production — fix it immediately. Use at least the authenticated-user pattern. - Add type and size validation in the rules. Never trust the frontend alone. Quick test: try uploading a .exe file — if it passes, your rules are broken.
- Implement upload with UID-based paths and progress. If you already use the SDK, verify that the path exactly matches the
match /users/{userId}/...rule.
For a complete overview of the Firebase ecosystem, read our pillar guide on Firebase for web and mobile apps (Italian).