Moltbook Security Breach: How 1.5 Million API Keys Were Exposed
Problem
When I read about the Moltbook security breach, I found this critical issue:
SECURITY BREACH: Moltbook AI Agent Platform- 1.5 million API keys exposed- Private AI agent communications accessible without authentication- User email addresses leaked- Root cause: Misconfigured Supabase database
Discovered by: Wiz security research teamImpact: Critical - Full access to AI agents and integrated servicesThe core problem: A misconfigured Supabase database left sensitive data exposed to anyone who knew where to look. No authentication required.
Environment
- Supabase (PostgreSQL-based Firebase alternative)
- Row Level Security (RLS) - Supabase’s security feature
- Service role keys vs. anonymous keys
- AI agent platform architecture
- API key storage patterns
What happened?
Moltbook, an AI agent platform, built their application on Supabase. The platform stores:
- API keys from users (OpenAI, GitHub, Stripe, etc.)
- Private messages between AI agents
- User email addresses and account data
The database configuration looked like this:
-- Tables were created but security was not properly configuredCREATE TABLE agent_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id), agent_id UUID, message TEXT, created_at TIMESTAMP DEFAULT NOW());
CREATE TABLE api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id), service_name TEXT, key_value TEXT, -- Stored in plain text! created_at TIMESTAMP DEFAULT NOW());
CREATE TABLE users ( id UUID PRIMARY KEY REFERENCES auth.users(id), email TEXT, created_at TIMESTAMP DEFAULT NOW());
-- RLS was NEVER enabled!-- ALTER TABLE agent_messages ENABLE ROW LEVEL SECURITY;When a security researcher at Wiz tested the API, they found they could query all data without authentication:
// No authentication required - anyone could do thisconst supabase = createClient( 'https://moltbook.supabase.co', 'public-anon-key' // This key is public by design)
// Fetch all private agent messagesconst { data: messages } = await supabase .from('agent_messages') .select('*')
// Fetch all API keysconst { data: keys } = await supabase .from('api_keys') .select('*')
// Fetch all user emailsconst { data: users } = await supabase .from('users') .select('email')The result: Complete access to 1.5 million API keys, private AI agent conversations, and user emails.
How to solve it?
I need to fix this vulnerability step by step.
Step 1: Enable Row Level Security
First, I’ll enable RLS on all sensitive tables:
-- Enable RLS on all tablesALTER TABLE agent_messages ENABLE ROW LEVEL SECURITY;ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY;ALTER TABLE users ENABLE ROW LEVEL SECURITY;Step 2: Create RLS Policies
Now I’ll create policies that restrict access:
-- Policy: Users can only see their own agent messagesCREATE POLICY "Users can view own messages" ON agent_messages FOR SELECT USING (auth.uid() = user_id);
-- Policy: Users can only insert their own messagesCREATE POLICY "Users can insert own messages" ON agent_messages FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Policy: Users can only see their own API keysCREATE POLICY "Users can view own API keys" ON api_keys FOR SELECT USING (auth.uid() = user_id);
-- Policy: Users can only insert their own API keysCREATE POLICY "Users can insert own API keys" ON api_keys FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Policy: Service role can bypass RLS for background jobsCREATE POLICY "Service role full access" ON agent_messages TO service_role USING (true) WITH CHECK (true);
CREATE POLICY "Service role full access on API keys" ON api_keys TO service_role USING (true) WITH CHECK (true);Step 3: Use the Correct Supabase Client
On the client side, I must use the anonymous key (not service role):
// CORRECT: Client-side uses anon keyimport { createClient } from '@supabase/supabase-js'
const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // Public key)
// RLS policies on server enforce what this user can accessconst { data } = await supabase .from('agent_messages') .select('*')On the server side, I can use service role but must still verify the user:
// CORRECT: Server-side validates user sessionimport { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs'
export default async function handler(req, res) { const supabase = createServerSupabaseClient({ req, res })
// Verify user is authenticated const { data: { session }, } = await supabase.auth.getSession()
if (!session) { return res.status(401).json({ error: 'Unauthorized' }) }
// RLS policies ensure user can only see their own data const { data } = await supabase .from('agent_messages') .select('*') .eq('user_id', session.user.id)
res.status(200).json(data)}Step 4: Encrypt API Keys at Rest
I should never store API keys in plain text:
import { encrypt, decrypt } from './crypto'
async function storeApiKey(userId: string, key: string) { // Encrypt the key before storing const encryptedKey = await encrypt(key, process.env.ENCRYPTION_KEY!)
await db.apiKeys.create({ data: { userId, encryptedKey, lastFour: key.slice(-4), // Store last 4 for identification only service: extractServiceName(key) } })}
async function getApiKey(userId: string, keyId: string) { const keyRecord = await db.apiKeys.findFirst({ where: { id: keyId, userId } })
if (!keyRecord) { throw new Error('API key not found') }
// Decrypt the key for use return await decrypt(keyRecord.encryptedKey, process.env.ENCRYPTION_KEY!)}Step 5: Test Security Configuration
I should always test my security assumptions:
export async function auditSupabaseSecurity(supabase: SupabaseClient) { const issues = []
// Check 1: Can we access data without authentication? const publicClient = createClient( supabaseUrl, supabaseAnonKey )
const { data: testData } = await publicClient .from('agent_messages') .select('*') .limit(1)
if (testData && testData.length > 0) { issues.push({ severity: 'CRITICAL', message: 'Public client can access agent_messages without auth' }) }
// Check 2: Are API keys stored in plain text? const { data: keySample } = await supabase .from('api_keys') .select('key_value') .limit(1)
if (keySample?.[0]?.key_value?.startsWith('sk-')) { issues.push({ severity: 'CRITICAL', message: 'API keys stored in plain text, not encrypted' }) }
// Check 3: Is service role key exposed in browser? if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE) { issues.push({ severity: 'CRITICAL', message: 'Service role key exposed in browser code' }) }
return issues}The reason
I think the key reasons for the Moltbook breach are:
1. Row Level Security was never enabled
- Supabase tables default to no access restrictions
- Without RLS, anyone with the public anon key can query all data
- RLS is opt-in, not automatic - you must explicitly enable it
2. Service role key may have been exposed on the client
- Service role keys bypass all RLS policies
- If this key was in browser code, complete database access was possible
- Service role keys should only be used in trusted server environments
3. API keys stored in plain text
- Even if RLS was enabled, storing keys in plain text is risky
- A single vulnerability exposes all keys
- Encryption at rest provides defense in depth
4. No security testing
- The vulnerability was discovered by external researchers
- Internal security testing should have caught this
- Automated security scans would have identified open database access
Summary
In this post, I explained the Moltbook security breach where a misconfigured Supabase database exposed 1.5 million API keys and private AI agent communications. The key point is that database security requires explicit configuration - Row Level Security policies must be enabled and properly configured, service role keys must never be exposed on the client side, and API keys must be encrypted at rest.
If you’re using Supabase or any database-as-a-service:
- Enable RLS on all tables with sensitive data
- Create policies that restrict data access to authenticated users
- Never use service role keys in client-side code
- Encrypt sensitive data like API keys before storage
- Test your security assumptions - try to access data without authentication
- Audit your database permissions regularly
For users affected by the Moltbook breach: Rotate all API keys that were used with the platform, check audit logs on integrated services for unauthorized access, and be vigilant for phishing emails.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Supabase Row Level Security Documentation
- 👨💻 Wiz Research Blog
- 👨💻 API Security Best Practices
- 👨💻 Supabase Security Checklist
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments