Skip to content

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 team
Impact: Critical - Full access to AI agents and integrated services

The 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:

database-schema.sql
-- Tables were created but security was not properly configured
CREATE 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:

attack-script.ts
// No authentication required - anyone could do this
const supabase = createClient(
'https://moltbook.supabase.co',
'public-anon-key' // This key is public by design
)
// Fetch all private agent messages
const { data: messages } = await supabase
.from('agent_messages')
.select('*')
// Fetch all API keys
const { data: keys } = await supabase
.from('api_keys')
.select('*')
// Fetch all user emails
const { 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.sql
-- Enable RLS on all tables
ALTER 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:

create-policies.sql
-- Policy: Users can only see their own agent messages
CREATE POLICY "Users can view own messages"
ON agent_messages
FOR SELECT
USING (auth.uid() = user_id);
-- Policy: Users can only insert their own messages
CREATE 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 keys
CREATE 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 keys
CREATE 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 jobs
CREATE 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):

"client-setup.tsx
// CORRECT: Client-side uses anon key
import { 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 access
const { data } = await supabase
.from('agent_messages')
.select('*')

On the server side, I can use service role but must still verify the user:

"server-setup.ts
// CORRECT: Server-side validates user session
import { 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:

encrypt-keys.ts
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:

"security-audit.ts
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:

  1. Enable RLS on all tables with sensitive data
  2. Create policies that restrict data access to authenticated users
  3. Never use service role keys in client-side code
  4. Encrypt sensitive data like API keys before storage
  5. Test your security assumptions - try to access data without authentication
  6. 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments