Loading...
Loading...
Agent skill that audits vibe-coded apps for common security vulnerabilities introduced by AI coding assistants
npx skill4agent add aradotso/security-skills vibe-security-skillSkill by ara.so — Security Skills collection.
npx skills add https://github.com/raroque/vibe-security-skill --skill vibe-securitynpx skills add https://github.com/raroque/vibe-security-skill --skill vibe-security# Project-level
git clone https://github.com/raroque/vibe-security-skill.git
cp -r vibe-security-skill/vibe-security/ .claude/skills/vibe-security/
# Global installation
cp -r vibe-security-skill/vibe-security/ ~/.claude/skills/vibe-security//vibe-security$vibe-security// ❌ Hardcoded secret
const supabase = createClient(
'https://xxx.supabase.co',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
)
// ❌ Exposed in client bundle
const OPENAI_API_KEY = 'sk-proj-...'// ✅ Environment variable
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// ✅ Server-side only
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY // Not NEXT_PUBLIC_
})-- ❌ RLS disabled
CREATE TABLE user_data (
id uuid,
user_id uuid,
sensitive_data text
);
-- No ALTER TABLE ... ENABLE ROW LEVEL SECURITY
-- ❌ Allows everything
CREATE POLICY "allow_all" ON user_data
FOR ALL USING (true);-- ✅ RLS enabled with proper policies
CREATE TABLE user_data (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users NOT NULL,
sensitive_data text
);
ALTER TABLE user_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY "users_select_own" ON user_data
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "users_insert_own" ON user_data
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "users_update_own" ON user_data
FOR UPDATE USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);// ❌ Decoding without verification
import jwt from 'jsonwebtoken'
const decoded = jwt.decode(token) // No signature check!
const userId = decoded.sub
// ❌ Middleware-only auth
// middleware.ts
export function middleware(req: NextRequest) {
const token = req.cookies.get('token')
if (!token) return NextResponse.redirect('/login')
}
// app/api/sensitive/route.ts - NOT protected!
export async function GET() {
return NextResponse.json(await db.getAllUserData())
}// ✅ Verify JWT signature
import jwt from 'jsonwebtoken'
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload
const userId = decoded.sub
// ✅ Auth in every API route
// lib/auth.ts
export async function requireAuth(req: Request) {
const token = req.headers.get('authorization')?.replace('Bearer ', '')
if (!token) throw new Error('Unauthorized')
const decoded = jwt.verify(token, process.env.JWT_SECRET!)
return decoded
}
// app/api/sensitive/route.ts
export async function GET(req: Request) {
const user = await requireAuth(req)
return NextResponse.json(await db.getUserData(user.sub))
}// ❌ Client submits price
export async function POST(req: Request) {
const { amount, productId } = await req.json()
const session = await stripe.checkout.sessions.create({
line_items: [{
price_data: {
currency: 'usd',
product: productId,
unit_amount: amount // ❌ Trusting client!
},
quantity: 1
}],
mode: 'payment'
})
}// ✅ Server determines price
const PRICES = {
'basic': 999,
'pro': 2999,
'enterprise': 9999
} as const
export async function POST(req: Request) {
const { plan } = await req.json()
if (!PRICES[plan]) throw new Error('Invalid plan')
const session = await stripe.checkout.sessions.create({
line_items: [{
price_data: {
currency: 'usd',
product: plan,
unit_amount: PRICES[plan] // ✅ Server-controlled
},
quantity: 1
}],
mode: 'payment'
})
}
// ✅ Verify webhook signatures
export async function POST(req: Request) {
const body = await req.text()
const sig = req.headers.get('stripe-signature')!
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
)
// Process event...
}// ❌ No rate limiting on expensive endpoints
export async function POST(req: Request) {
const { prompt } = await req.json()
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }]
})
return NextResponse.json(completion)
}// ✅ Server-side rate limiting
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '1 h'),
analytics: true
})
export async function POST(req: Request) {
const user = await requireAuth(req)
const { success } = await ratelimit.limit(user.sub)
if (!success) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429 }
)
}
const { prompt } = await req.json()
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
max_tokens: 1000 // ✅ Cap token usage
})
return NextResponse.json(completion)
}// ❌ API key in JS bundle
const OPENAI_API_KEY = 'sk-proj-...'
// ❌ Token in AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage'
await AsyncStorage.setItem('auth_token', token)// ✅ Use backend proxy for AI calls
const response = await fetch('https://api.myapp.com/ai/chat', {
method: 'POST',
headers: {
'Authorization': `Bearer ${userToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ prompt })
})
// ✅ Use secure storage for tokens
import * as SecureStore from 'expo-secure-store'
await SecureStore.setItemAsync('auth_token', token)
const token = await SecureStore.getItemAsync('auth_token')// ❌ SQL injection vulnerability
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const userId = searchParams.get('userId')
const result = await db.$queryRawUnsafe(
`SELECT * FROM users WHERE id = ${userId}`
)
}
// ❌ Prisma operator injection
const users = await prisma.user.findMany({
where: req.query // ❌ Direct user input
})// ✅ Parameterized queries
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const userId = searchParams.get('userId')
const result = await db.$queryRaw`
SELECT * FROM users WHERE id = ${userId}
`
}
// ✅ Validate and sanitize input
const userIdSchema = z.string().uuid()
const userId = userIdSchema.parse(searchParams.get('userId'))
const user = await prisma.user.findUnique({
where: { id: userId }
}).claude/skills/vibe-security/rules//vibe-security$vibe-security# .env.local (never commit)
DATABASE_URL="postgresql://..."
STRIPE_SECRET_KEY="sk_test_..."
OPENAI_API_KEY="sk-proj-..."
# Public vars (safe in client bundle)
NEXT_PUBLIC_SUPABASE_URL="https://xxx.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJ..."// next.config.js
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
}
]
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders
}
]
}
}import { z } from 'zod'
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(13).max(120)
})
export async function POST(req: Request) {
const body = await req.json()
const validated = createUserSchema.parse(body)
// Safe to use validated data
await db.user.create({ data: validated })
}