07Data
Database Security
Database Security — Instruction 07
Coverage
CWE-89, CWE-862, CWE-269 — Database security for all stacks Firebase, Supabase, PostgreSQL, MySQL, MongoDB, Redis
Firebase Security
1. Firestore Rules — Never Allow All
// 🔴 CRITICAL — World-readable and writable
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true; // ANYONE can read/write EVERYTHING
}
}
}
// 🟢 CORRECT — Authentication required, ownership enforced
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
match /posts/{postId} {
allow read: if request.auth != null;
allow write: if request.auth != null && request.auth.uid == resource.data.authorId;
}
}
}
2. Firebase Storage Rules
// 🔴 CRITICAL
allow read, write: if true;
// 🟢 CORRECT
match /users/{userId}/{allPaths=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
allow read: if resource.size < 5 * 1024 * 1024; // max 5MB reads
}
3. Firebase API Key Restrictions
- Firebase API key exposed in frontend JS = EXPECTED (it's public by design)
- BUT: restrict key in Firebase Console → API restrictions → specific APIs only
- Enable App Check for production
Supabase Security
4. Row Level Security (RLS)
-- 🔴 CRITICAL — No RLS = all authenticated users see all data
-- Check if RLS is enabled on all tables:
SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';
-- 🟢 Enable RLS on every table
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 🟢 Add policies
CREATE POLICY "Users can only see their own data"
ON users FOR ALL
USING (auth.uid() = id);
CREATE POLICY "Authenticated users can read posts"
ON posts FOR SELECT
USING (auth.role() = 'authenticated');
5. Supabase Service Role Key
// 🔴 CRITICAL — Service role key bypasses ALL RLS!
SUPABASE_SERVICE_ROLE_KEY exposed in frontend code
// 🟢 Service role key = server-side ONLY
// Public key (anon key) = safe for frontend
// Service role key = NEVER in frontend code
SQL (General)
6. Parameterized Queries
// 🔴 SQL injection
const query = `SELECT * FROM users WHERE email = '${email}'`
// 🟢 Parameterized
const { rows } = await pool.query('SELECT * FROM users WHERE email = $1', [email])
// 🟢 ORM (auto-parameterized)
const user = await User.findOne({ where: { email } }) // Sequelize
const user = await prisma.user.findUnique({ where: { email } }) // Prisma
7. SQL Truncation Attack
// 🔴 Long input truncated by DB may match existing user
// Validate string length BEFORE database insertion
if (username.length > 50) return res.status(400)
// Enable strict mode in MySQL: SET sql_mode = 'STRICT_TRANS_TABLES'
8. Least Privilege DB User
// 🔴 App using root/superuser DB account
DB_USER=root
// 🟢 Dedicated app user with minimal permissions
// CREATE USER 'appuser'@'%' IDENTIFIED BY 'strong_password';
// GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'appuser'@'%';
// NEVER GRANT: CREATE, DROP, ALTER, SUPER
9. DB Connection String Security
// 🔴 Credentials in code or public config
const db = new Pool({ connectionString: 'postgresql://root:password@localhost/mydb' })
// 🟢 Environment variables only
const db = new Pool({ connectionString: process.env.DATABASE_URL })
10. SSL for DB Connections
// Production DB connection must use SSL
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: true } // verify cert
})
11. Database Port Not Public
Check:
- PostgreSQL 5432 not exposed to internet
- MySQL 3306 not exposed to internet
- MongoDB 27017 not exposed to internet Use VPC/firewall rules to restrict to app servers only.
MongoDB
12. MongoDB Authentication
// 🔴 No auth (default in some setups)
mongodb://localhost:27017/mydb
// 🟢 With auth
mongodb://appuser:password@localhost:27017/mydb?authSource=admin
13. NoSQL Injection
// 🔴 User input directly in MongoDB query
User.find({ email: req.body.email })
// Attacker sends: { "$gt": "" } → returns ALL users
// 🟢 Validate and sanitize
const email = String(req.body.email) // force string
if (!isValidEmail(email)) return res.status(400)
User.find({ email })
Redis
14. Redis Authentication
// 🔴 No password (common default)
// 🟢 requirepass in redis.conf
requirepass YourStrongPasswordHere
// 🔴 Redis exposed on 0.0.0.0
// 🟢 Bind to localhost only
bind 127.0.0.1
15. Sensitive Data Expiry in Redis
// All cached sensitive data must have TTL
client.set('session:' + id, data, 'EX', 3600) // expires in 1 hour
// Never: client.set('session:' + id, data) // no expiry
PII in Database
16. Sensitive Field Encryption at Rest
// Fields that MUST be encrypted before storage:
// creditCard, ssn, passport, bankAccount
// 🟢 Application-level encryption
const encrypted = encrypt(creditCard, process.env.ENCRYPTION_KEY)
await db.users.update({ creditCardEncrypted: encrypted })
// Never store in plaintext: password (hash), creditCard, SSN, bank info
17. Database Enumeration Prevention
// 🔴 Error reveals DB schema
catch (err) {
res.json({ error: err.message }) // "column 'email' does not exist"
}
// 🟢 Generic error to user, detailed to logs only
catch (err) {
logger.error(err) // full error in logs
res.status(500).json({ error: 'Internal server error' }) // generic to user
}