-
Notifications
You must be signed in to change notification settings - Fork 0
Security Guide
Essential security practices and guidelines for deploying SideQuest safely.
DO NOT deploy SideQuest to production with default security settings! The default configuration is insecure and intended only for local development.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 3, 1);
}
}
}Problems:
- Anyone can read all data
- Anyone can write/delete any data
- No authentication checks
- No input validation
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions
function isSignedIn() {
return request.auth != null;
}
function isOwner(userId) {
return isSignedIn() && request.auth.uid == userId;
}
function hasValidTaskData() {
return request.resource.data.title is string
&& request.resource.data.title.size() >= 3
&& request.resource.data.title.size() <= 100
&& request.resource.data.description is string
&& request.resource.data.description.size() >= 10
&& request.resource.data.description.size() <= 1000
&& request.resource.data.category in ['Help', 'Delivery', 'Social', 'Other']
&& request.resource.data.reward is number
&& request.resource.data.reward >= 0
&& request.resource.data.reward <= 10000;
}
// Users collection
match /users/{userId} {
// Anyone can read user profiles (for task display)
allow read: if isSignedIn();
// Users can only create their own profile
allow create: if isSignedIn() && isOwner(userId);
// Users can only update their own profile
// Prevent wallet manipulation
allow update: if isOwner(userId)
&& (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['wallet', 'uid', 'email']));
// Users cannot delete their profile
allow delete: if false;
}
// Tasks collection
match /tasks/{taskId} {
// Anyone signed in can read tasks
allow read: if isSignedIn();
// Users can create tasks with valid data
allow create: if isSignedIn()
&& hasValidTaskData()
&& request.resource.data.postedBy == request.auth.uid
&& request.resource.data.status == 'open'
&& request.resource.data.claimedBy == null;
// Task owners can update their open tasks
// Task claimers can update in-progress tasks
allow update: if isSignedIn() && (
// Owner can update/delete open tasks
(isOwner(resource.data.postedBy) && resource.data.status == 'open')
// Claimer can mark as complete
|| (isOwner(resource.data.claimedBy) && resource.data.status == 'in-progress')
// Anyone can claim open tasks
|| (resource.data.status == 'open'
&& request.resource.data.status == 'in-progress'
&& request.resource.data.claimedBy == request.auth.uid)
);
// Task owners can delete their open tasks
allow delete: if isSignedIn()
&& isOwner(resource.data.postedBy)
&& resource.data.status == 'open';
}
// Transactions collection (audit log)
match /transactions/{transactionId} {
// Users can read their own transactions
allow read: if isSignedIn() && isOwner(resource.data.userId);
// Only server/cloud functions can create transactions
allow create, update, delete: if false;
}
}
}# Install Firebase CLI
npm install -g firebase-tools
# Login to Firebase
firebase login
# Initialize Firebase in your project
firebase init firestore
# Deploy rules
firebase deploy --only firestore:rulesUse Firebase Emulator Suite for local testing:
# Install emulators
firebase init emulators
# Start emulators
firebase emulators:start
# Update firebase-config.js to use emulator
import { connectFirestoreEmulator } from "firebase/firestore";
connectFirestoreEmulator(db, 'localhost', 8080);Enforce strong passwords in your UI:
function validatePassword(password) {
if (password.length < 8) {
throw new Error('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
throw new Error('Password must contain uppercase letter');
}
if (!/[a-z]/.test(password)) {
throw new Error('Password must contain lowercase letter');
}
if (!/[0-9]/.test(password)) {
throw new Error('Password must contain number');
}
return true;
}Enable email verification in Firebase:
import { sendEmailVerification } from "firebase/auth";
async function handleSignup(name, email, password) {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
// Send verification email
await sendEmailVerification(userCredential.user);
showToast('Please verify your email to continue', 'info');
}
// Check verification status
function isEmailVerified() {
return auth.currentUser && auth.currentUser.emailVerified;
}Configure session persistence:
import {
browserLocalPersistence,
browserSessionPersistence,
setPersistence
} from "firebase/auth";
// Keep user signed in across browser sessions
await setPersistence(auth, browserLocalPersistence);
// Or sign out when browser closes
await setPersistence(auth, browserSessionPersistence);Always validate user input before sending to Firebase:
function validateTaskInput(data) {
const errors = [];
// Title validation
if (!data.title || typeof data.title !== 'string') {
errors.push('Title is required');
} else if (data.title.length < 3) {
errors.push('Title must be at least 3 characters');
} else if (data.title.length > 100) {
errors.push('Title must be less than 100 characters');
}
// Description validation
if (!data.description || typeof data.description !== 'string') {
errors.push('Description is required');
} else if (data.description.length < 10) {
errors.push('Description must be at least 10 characters');
} else if (data.description.length > 1000) {
errors.push('Description must be less than 1000 characters');
}
// Category validation
const validCategories = ['Help', 'Delivery', 'Social', 'Other'];
if (!validCategories.includes(data.category)) {
errors.push('Invalid category');
}
// Reward validation
if (typeof data.reward !== 'number' || data.reward < 0) {
errors.push('Reward must be a positive number');
} else if (data.reward > 10000) {
errors.push('Reward cannot exceed ₹10,000');
}
if (errors.length > 0) {
throw new Error(errors.join(', '));
}
return true;
}Prevent XSS attacks by sanitizing user-generated content:
// ✅ Safe: Use textContent
element.textContent = userInput;
// ❌ Dangerous: innerHTML with user input
element.innerHTML = userInput;
// If HTML is needed, sanitize it first
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);Important: Firebase API keys are not secret. They identify your Firebase project, not authorize access. Security is handled by Firestore rules.
However, you should still:
- Restrict API Keys: In Google Cloud Console, restrict your API key to specific domains
- Use App Check: Verify requests come from your app, not bots
import { initializeAppCheck, ReCaptchaV3Provider } from "firebase/app-check";
// Initialize App Check
const appCheck = initializeAppCheck(app, {
provider: new ReCaptchaV3Provider('YOUR_RECAPTCHA_SITE_KEY'),
isTokenAutoRefreshEnabled: true
});For sensitive configuration, use environment variables:
// .env (never commit this file)
VITE_FIREBASE_API_KEY=your-api-key
VITE_FIREBASE_PROJECT_ID=your-project-id
// firebase-config.js
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
// ...
};Use Firestore transactions for atomic wallet operations:
async function createTaskWithEscrow(taskData, userId, rewardAmount) {
const userRef = doc(db, 'users', userId);
try {
await runTransaction(db, async (transaction) => {
const userDoc = await transaction.get(userRef);
if (!userDoc.exists()) {
throw new Error('User not found');
}
const currentBalance = userDoc.data().wallet || 0;
if (currentBalance < rewardAmount) {
throw new Error('Insufficient balance');
}
// Deduct from wallet
transaction.update(userRef, {
wallet: currentBalance - rewardAmount
});
// Create task
const taskRef = doc(collection(db, 'tasks'));
transaction.set(taskRef, {
...taskData,
escrowHeld: true,
reward: rewardAmount
});
// Log transaction
const txnRef = doc(collection(db, 'transactions'));
transaction.set(txnRef, {
userId,
type: 'escrow_deduct',
amount: rewardAmount,
taskId: taskRef.id,
timestamp: serverTimestamp()
});
});
showToast('Task created and payment held', 'success');
} catch (error) {
console.error('Transaction failed:', error);
showToast('Failed to create task: ' + error.message, 'error');
}
}Ensure wallet updates are atomic:
// ❌ Bad: Race condition possible
const balance = await getBalance(userId);
await updateBalance(userId, balance - amount);
// ✅ Good: Atomic transaction
await runTransaction(db, async (transaction) => {
const userDoc = await transaction.get(userRef);
const balance = userDoc.data().wallet;
transaction.update(userRef, {
wallet: balance - amount
});
});Prevent spam by limiting actions:
class RateLimiter {
constructor(maxCalls, windowMs) {
this.maxCalls = maxCalls;
this.windowMs = windowMs;
this.calls = [];
}
isAllowed() {
const now = Date.now();
this.calls = this.calls.filter(time => now - time < this.windowMs);
if (this.calls.length >= this.maxCalls) {
return false;
}
this.calls.push(now);
return true;
}
}
// Usage: Max 5 task creations per minute
const taskLimiter = new RateLimiter(5, 60000);
async function createTask(data) {
if (!taskLimiter.isAllowed()) {
showToast('Too many tasks created. Please wait.', 'error');
return;
}
// Proceed with task creation
// ...
}Use Firebase App Check and Cloud Functions:
// Cloud Function with rate limiting
const functions = require('firebase-functions');
const admin = require('firebase-admin');
exports.createTask = functions.https.onCall(async (data, context) => {
// Verify App Check
if (context.app == undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called from an App Check verified app.'
);
}
// Check rate limit (using Firestore or Redis)
const userId = context.auth.uid;
const recentTasks = await admin.firestore()
.collection('tasks')
.where('postedBy', '==', userId)
.where('createdAt', '>', Date.now() - 60000)
.get();
if (recentTasks.size >= 5) {
throw new functions.https.HttpsError(
'resource-exhausted',
'Too many tasks created'
);
}
// Create task
// ...
});Always use HTTPS in production:
// Redirect HTTP to HTTPS
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
location.replace(`https:${location.href.substring(location.protocol.length)}`);
}Firebase Hosting handles CORS automatically. For custom domains:
// firebase.json
{
"hosting": {
"headers": [
{
"source": "/api/**",
"headers": [
{
"key": "Access-Control-Allow-Origin",
"value": "https://your-domain.com"
}
]
}
]
}
}Log security-relevant events:
async function logSecurityEvent(type, details) {
await addDoc(collection(db, 'security_logs'), {
type,
details,
userId: auth.currentUser?.uid || 'anonymous',
timestamp: serverTimestamp(),
userAgent: navigator.userAgent,
ip: await fetch('https://api.ipify.org?format=json')
.then(r => r.json())
.then(d => d.ip)
});
}
// Usage
logSecurityEvent('failed_login', { email });
logSecurityEvent('wallet_updated', { oldBalance, newBalance });
logSecurityEvent('task_deleted', { taskId });Enable audit logs in Google Cloud Console:
- Go to IAM & Admin > Audit Logs
- Enable "Data Access" logs for Firestore
- Monitor in Logs Explorer
If you suspect a security breach:
-
Immediately disable affected accounts
firebase auth:delete USER_UID
-
Review audit logs
- Check Firestore access patterns
- Review Cloud Functions logs
- Check authentication logs
-
Update security rules
- Deploy stricter rules immediately
- Review and fix vulnerabilities
-
Notify affected users
- Be transparent about the breach
- Explain steps taken
- Recommend password changes
-
Post-mortem
- Document what happened
- Update security practices
- Implement preventive measures
Before deploying to production:
- Updated Firestore security rules (no wildcards)
- Enabled Firebase App Check
- Restricted API keys to specific domains
- Enabled email verification
- Configured password requirements
- Set up audit logging
- All user input validated client-side
- All user input validated server-side (rules)
- No secrets in client-side code
- XSS prevention implemented
- CSRF protection in place
- Rate limiting implemented
- HTTPS enforced
- User data properly encrypted
- Wallet transactions are atomic
- No PII exposed in logs
- Backup strategy in place
- Security logs enabled
- Alerts configured for suspicious activity
- Regular security audits scheduled