Common Security Mistakes to Avoid
Learning from Others' Mistakes
Security incidents often stem from predictable mistakes. By understanding common vulnerabilities, you can avoid them in your own applications.
Mistake 1: Forgetting to Enable RLS
The most common and dangerous mistake:
-- Table created without RLS
CREATE TABLE user_secrets (
id uuid PRIMARY KEY,
user_id uuid REFERENCES auth.users(id),
secret_data text
);
-- Without RLS enabled, ANYONE can query this table!
SELECT * FROM user_secrets; -- Returns everything
The Fix
-- Always enable RLS immediately after creating a table
CREATE TABLE user_secrets (...);
ALTER TABLE user_secrets ENABLE ROW LEVEL SECURITY;
-- Now the table is inaccessible until policies are added
Prevention Habit
Make it a checklist item:
- CREATE TABLE
- ALTER TABLE ... ENABLE ROW LEVEL SECURITY
- CREATE POLICY ...
Mistake 2: Trusting Client-Provided IDs
// WRONG: Client sends user_id
const { error } = await supabase
.from('posts')
.insert({
user_id: body.userId, // Attacker can send ANY user_id!
title: body.title
})
The Fix
Let RLS enforce ownership:
CREATE POLICY "Users can only insert own posts"
ON posts FOR INSERT
WITH CHECK (user_id = auth.uid());
// RIGHT: Client doesn't send user_id, it's from auth
const { error } = await supabase
.from('posts')
.insert({
user_id: user.id, // Or let database default handle it
title: body.title
})
Mistake 3: Overly Permissive Policies
-- WRONG: Too broad
CREATE POLICY "Allow all authenticated users"
ON sensitive_data FOR ALL
USING (auth.role() = 'authenticated');
-- This lets ANY logged-in user read/write ALL data!
The Fix
Be specific about who can access what:
-- RIGHT: Only owners access their data
CREATE POLICY "Users access own data"
ON sensitive_data FOR ALL
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
Mistake 4: Missing WITH CHECK on Updates
-- WRONG: Missing WITH CHECK
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (user_id = auth.uid());
-- No WITH CHECK!
-- This allows:
UPDATE posts SET user_id = 'other-user-id' WHERE id = 'my-post';
-- User can transfer ownership or steal others' posts!
The Fix
-- RIGHT: WITH CHECK prevents changing ownership
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
Mistake 5: Exposing Service Role Key
// CATASTROPHIC: Service key in frontend
const supabase = createClient(
url,
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' // service_role key!
)
// Now anyone can:
// - Read ALL data (bypasses RLS)
// - Modify ALL data
// - Delete ALL data
// - Access auth.users table directly
The Fix
- Only use anon key in frontend
- service_role only in secure server environments
- Audit your codebase:
grep -r "service_role" .
Mistake 6: Not Validating Input Types
-- WRONG: No type validation
CREATE TABLE orders (
quantity text, -- Should be integer!
price text -- Should be numeric!
);
-- User could insert: quantity = 'DROP TABLE orders;--'
The Fix
-- RIGHT: Proper types and constraints
CREATE TABLE orders (
quantity integer NOT NULL CHECK (quantity > 0),
price numeric(10,2) NOT NULL CHECK (price >= 0)
);
Mistake 7: Insecure Direct Object References (IDOR)
// WRONG: No ownership check
async function deletePost(postId) {
// Any user can delete any post by guessing IDs!
await supabase.from('posts').delete().eq('id', postId)
}
The Fix
RLS ensures ownership:
CREATE POLICY "Users can only delete own posts"
ON posts FOR DELETE
USING (user_id = auth.uid());
Now even if someone guesses a post ID, they can only delete it if they own it.
Mistake 8: Storing Secrets in the Database
-- WRONG: Plain text secrets
CREATE TABLE api_integrations (
user_id uuid,
api_key text, -- Plain text!
api_secret text -- Plain text!
);
The Fix
Encrypt sensitive data:
-- RIGHT: Encrypted secrets
CREATE EXTENSION pgcrypto;
CREATE TABLE api_integrations (
user_id uuid,
api_key_encrypted bytea,
api_secret_encrypted bytea
);
-- Encrypt on insert
INSERT INTO api_integrations (user_id, api_key_encrypted)
VALUES (
auth.uid(),
pgp_sym_encrypt('actual-api-key', current_setting('app.encryption_key'))
);
Or better: use a secrets manager and store references.
Mistake 9: Not Rate Limiting
// WRONG: No rate limiting on sensitive operations
async function forgotPassword(email) {
// Attacker can spam this endpoint to:
// 1. Drain your email quota
// 2. Harass users with emails
// 3. Enumerate valid emails
await supabase.auth.resetPasswordForEmail(email)
}
The Fix
Implement rate limiting at application or infrastructure level:
// Edge Function with rate limiting
import { RateLimiter } from './rate-limiter'
const limiter = new RateLimiter({ points: 3, duration: 60 })
serve(async (req) => {
const ip = req.headers.get('x-forwarded-for')
try {
await limiter.consume(ip)
} catch {
return new Response('Too many requests', { status: 429 })
}
// Process request...
})
Mistake 10: Leaking Information in Errors
// WRONG: Detailed error messages
if (!user) {
return res.status(401).json({
error: 'User not found with email: ' + email
})
}
if (!bcrypt.compare(password, user.password_hash)) {
return res.status(401).json({
error: 'Invalid password for user: ' + email
})
}
// Attacker now knows:
// - Which emails exist
// - That the email exists but password is wrong
The Fix
// RIGHT: Generic error messages
if (!user || !bcrypt.compare(password, user.password_hash)) {
return res.status(401).json({
error: 'Invalid email or password'
})
}
Security Checklist
Before deploying, verify:
- RLS enabled on all tables
- No service_role key in frontend code
- WITH CHECK on all UPDATE/INSERT policies
- Sensitive data encrypted
- Input validation at database level
- Rate limiting on sensitive endpoints
- Generic error messages
- No hardcoded secrets in code
- Environment variables properly configured
Key Takeaways
- Enable RLS on every table: No exceptions
- Never trust client input: Validate and constrain
- Use WITH CHECK: Prevent privilege escalation
- Keep service_role secret: Only server-side
- Encrypt sensitive data: Never store plain text secrets
- Rate limit sensitive operations: Prevent abuse
- Use generic errors: Don't leak information
Next Steps
With common mistakes understood, we'll provide a comprehensive production security checklist.
Security bugs are often boring—the same mistakes made by different people. The good news? You can avoid most of them by following established patterns and checklists.
Discussion
Sign in to join the discussion.

