Skip to content

Commit 1ca2d43

Browse files
authored
Merge branch 'main' into palette-collapsible-a11y-6862435676078644038
2 parents 26a2f3d + 78bc4bc commit 1ca2d43

41 files changed

Lines changed: 1159 additions & 692 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
name: vibe-security
3+
description: Audits codebases for common security vulnerabilities that AI coding assistants introduce in "vibe-coded" applications. Checks for exposed API keys, broken access control (Supabase RLS, Firebase rules), missing auth validation, client-side trust issues, insecure payment flows, and more. Use this skill whenever the user asks about security, wants a code review, mentions "vibe coding", or when you're writing or reviewing code that handles authentication, payments, database access, API keys, secrets, or user data — even if they don't explicitly mention security. Also trigger when the user says things like "is this safe?", "check my code", "audit this", "review for vulnerabilities", or "can someone hack this?".
4+
license: MIT
5+
metadata:
6+
author: Chris Raroque
7+
version: "1.0"
8+
---
9+
10+
Audit code for security vulnerabilities commonly introduced by AI code generation. These issues are prevalent in "vibe-coded" apps — projects built rapidly with AI assistance where security fundamentals get skipped.
11+
12+
AI assistants consistently get these patterns wrong, leading to real breaches, stolen API keys, and drained billing accounts. This skill exists to catch those mistakes before they ship.
13+
14+
15+
## The Core Principle
16+
17+
Never trust the client. Every price, user ID, role, subscription status, feature flag, and rate limit counter must be validated or enforced server-side. If it exists only in the browser, mobile bundle, or request body, an attacker controls it.
18+
19+
20+
## Audit Process
21+
22+
Examine the codebase systematically. For each step, load the relevant reference file only if the codebase uses that technology or pattern. Skip steps that aren't relevant.
23+
24+
1. **Secrets & Environment Variables** — Scan for hardcoded API keys, tokens, or credentials. Check for secrets exposed via client-side env var prefixes (`NEXT_PUBLIC_`, `VITE_`, `EXPO_PUBLIC_`). Verify `.env` is in `.gitignore`. See `references/secrets-and-env.md`.
25+
26+
2. **Database Access Control** — Check Supabase RLS policies, Firebase Security Rules, or Convex auth guards. This is the #1 source of critical vulnerabilities in vibe-coded apps. See `references/database-security.md`.
27+
28+
3. **Authentication & Authorization** — Validate JWT handling, middleware auth, Server Action protection, and session management. See `references/authentication.md`.
29+
30+
4. **Rate Limiting & Abuse Prevention** — Ensure auth endpoints, AI calls, and expensive operations have rate limits. Verify rate limit counters can't be tampered with. See `references/rate-limiting.md`.
31+
32+
5. **Payment Security** — Check for client-side price manipulation, webhook signature verification, and subscription status validation. See `references/payments.md`.
33+
34+
6. **Mobile Security** — Verify secure token storage, API key protection via backend proxy, and deep link validation. See `references/mobile.md`.
35+
36+
7. **AI / LLM Integration** — Check for exposed AI API keys, missing usage caps, prompt injection vectors, and unsafe output rendering. See `references/ai-integration.md`.
37+
38+
8. **Deployment Configuration** — Verify production settings, security headers, source map exposure, and environment separation. See `references/deployment.md`.
39+
40+
9. **Data Access & Input Validation** — Check for SQL injection, ORM misuse, and missing input validation. See `references/data-access.md`.
41+
42+
If doing a partial review or generating code in a specific area, load only the relevant reference files.
43+
44+
45+
## Core Instructions
46+
47+
- Report only genuine security issues. Do not nitpick style or non-security concerns.
48+
- When multiple issues exist, prioritize by exploitability and real-world impact.
49+
- If the codebase doesn't use a particular technology (e.g., no Supabase), skip that section entirely.
50+
- When generating new code, consult the relevant reference files proactively to avoid introducing vulnerabilities in the first place.
51+
- If you find a critical issue (exposed secrets, disabled RLS, auth bypass), flag it immediately at the top of your response — don't bury it in a long list.
52+
53+
54+
## Output Format
55+
56+
Organize findings by severity: **Critical****High****Medium****Low**.
57+
58+
For each issue:
59+
1. State the file and relevant line(s).
60+
2. Name the vulnerability.
61+
3. Explain what an attacker could do (concrete impact, not abstract risk).
62+
4. Show a before/after code fix.
63+
64+
Skip areas with no issues. End with a prioritized summary.
65+
66+
### Example Output
67+
68+
#### Critical
69+
70+
**`lib/supabase.ts:3` — Supabase `service_role` key exposed in client bundle**
71+
72+
The `service_role` key bypasses all Row-Level Security. Anyone can extract it from the browser bundle and read, modify, or delete every row in your database.
73+
74+
```typescript
75+
// Before
76+
const supabase = createClient(url, process.env.NEXT_PUBLIC_SUPABASE_SERVICE_KEY!)
77+
78+
// After — use the anon key client-side; service_role belongs only in server-side code
79+
const supabase = createClient(url, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)
80+
```
81+
82+
#### High
83+
84+
**`app/api/checkout/route.ts:15` — Price taken from client request body**
85+
86+
An attacker can set any price (including $0.01) by modifying the request. Prices must be looked up server-side.
87+
88+
```typescript
89+
// Before
90+
const session = await stripe.checkout.sessions.create({
91+
line_items: [{ price_data: { unit_amount: req.body.price } }]
92+
})
93+
94+
// After — look up the price server-side
95+
const product = await db.products.findUnique({ where: { id: req.body.productId } })
96+
const session = await stripe.checkout.sessions.create({
97+
line_items: [{ price: product.stripePriceId }]
98+
})
99+
```
100+
101+
### Summary
102+
103+
1. **Service role key exposed (Critical):** Anyone can bypass all database security. Rotate the key immediately and move it to server-side only.
104+
2. **Client-controlled pricing (High):** Attackers can purchase at any price. Use server-side price lookup.
105+
106+
107+
## When Generating Code
108+
109+
These rules also apply proactively. Before writing code that touches auth, payments, database access, API keys, or user data, consult the relevant reference file to avoid introducing the vulnerability in the first place. Prevention is better than detection.
110+
111+
112+
## References
113+
114+
- `references/secrets-and-env.md` — API keys, tokens, environment variable configuration, and `.gitignore` rules.
115+
- `references/database-security.md` — Supabase RLS, Firebase Security Rules, and Convex auth patterns.
116+
- `references/authentication.md` — JWT verification, middleware, Server Actions, and session management.
117+
- `references/rate-limiting.md` — Rate limiting strategies and abuse prevention.
118+
- `references/payments.md` — Stripe security, webhook verification, and price validation.
119+
- `references/mobile.md` — React Native and Expo security: secure storage, API proxy, deep links.
120+
- `references/ai-integration.md` — LLM API key protection, usage caps, prompt injection, and output sanitization.
121+
- `references/deployment.md` — Production configuration, security headers, and environment separation.
122+
- `references/data-access.md` — SQL injection prevention, ORM safety, and input validation.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
interface:
2+
display_name: "Vibe Security"
3+
short_description: "Audits vibe-coded apps for common security vulnerabilities."
4+
brand_color: "#DC2626"
5+
default_prompt: "Use $vibe-security to audit my project for security issues."
6+
7+
policy:
8+
allow_implicit_invocation: true
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# AI / LLM Integration Security
2+
3+
## API Keys Are Server-Side Only
4+
5+
AI API keys (OpenAI, Anthropic, Google, etc.) must never appear in client-side code. They allow unlimited API usage at your expense. A leaked key can drain thousands of dollars in minutes.
6+
7+
- No `NEXT_PUBLIC_OPENAI_API_KEY`
8+
- No API keys in React Native / Expo bundles
9+
- No API keys in client-side JavaScript
10+
11+
All AI API calls go through your backend. The client sends the user's message to your server; your server calls the AI API.
12+
13+
## Spending Caps
14+
15+
Set hard spending caps on every AI API provider:
16+
- OpenAI: Usage limits in dashboard
17+
- Anthropic: Spending limits in console
18+
- Google: Budget alerts in Cloud Console
19+
20+
Also implement **per-user usage limits** in your application:
21+
- Track token usage per user in your database
22+
- Set daily/monthly caps per user or per tier
23+
- Return a clear error when limits are exceeded
24+
- Don't rely on the AI provider's caps alone — they may have lag
25+
26+
## Prompt Injection
27+
28+
User input must be sanitized before inclusion in prompts. Never concatenate raw user input into system prompts:
29+
30+
```typescript
31+
// BAD: user can override system instructions
32+
const prompt = `You are a helpful assistant. User says: ${userInput}`;
33+
34+
// BETTER: separate system and user messages
35+
const messages = [
36+
{ role: 'system', content: 'You are a helpful assistant.' },
37+
{ role: 'user', content: userInput },
38+
];
39+
```
40+
41+
Even with separate messages, be aware that sophisticated prompt injection can still occur. For high-stakes applications, consider:
42+
- Input validation and filtering
43+
- Output validation before acting on LLM responses
44+
- Limiting the LLM's capabilities (no tool access for user-facing chat)
45+
46+
## LLM Output Is Untrusted
47+
48+
LLM responses should be treated as untrusted user input:
49+
50+
- **Sanitize before rendering as HTML** — LLM output can contain script tags or event handlers
51+
- **Never execute LLM output as code** without sandboxing
52+
- **Validate tool/function call parameters** — if using function calling, validate all returned parameters against an allowlist and schema before executing
53+
54+
## Tool / Function Calling
55+
56+
If your application gives an LLM access to tools (database queries, API calls, file operations):
57+
- Restrict operations to a safe allowlist
58+
- Validate all parameters from the LLM against a schema
59+
- Use least-privilege access (read-only where possible)
60+
- Log all tool invocations for audit
61+
- Never let the LLM construct raw SQL or shell commands from user input
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Authentication & Authorization
2+
3+
## JWT Handling
4+
5+
- **Use `jwt.verify()`, never `jwt.decode()` alone.** `decode` reads the payload without checking the signature — an attacker can forge any payload.
6+
- **Explicitly reject `"alg": "none"`.** Some JWT libraries accept unsigned tokens if the algorithm is set to `"none"`. Your verification must reject this.
7+
- **Validate issuer, audience, and expiration** — not just the signature.
8+
9+
```typescript
10+
// BAD: reads token without verifying signature
11+
const payload = jwt.decode(token);
12+
13+
// GOOD: verifies signature, rejects tampered tokens
14+
const payload = jwt.verify(token, secret, {
15+
algorithms: ['HS256'],
16+
issuer: 'your-app',
17+
});
18+
```
19+
20+
## Next.js Middleware Is Not Enough
21+
22+
Next.js middleware runs at the edge and is convenient for auth checks, but it is **not a reliable sole auth layer**. CVE-2025-29927 demonstrated that middleware could be completely bypassed via a spoofed `x-middleware-subrequest` header.
23+
24+
Always verify auth again in:
25+
- Server Actions
26+
- Route Handlers (`app/api/`)
27+
- Data access functions / database queries
28+
29+
Middleware should be a convenience layer, not the only wall between an attacker and your data.
30+
31+
## Server Actions Are Public Endpoints
32+
33+
Server Actions compile into public POST endpoints. Anyone can call them with `curl`. AI assistants frequently generate Server Actions that assume they're only called by the UI:
34+
35+
```typescript
36+
// BAD: no auth check, no input validation
37+
'use server';
38+
export async function deleteItem(id: string) {
39+
await db.items.delete({ where: { id } });
40+
}
41+
42+
// GOOD: validates input, authenticates, and authorizes
43+
'use server';
44+
export async function deleteItem(input: unknown) {
45+
const parsed = schema.safeParse(input);
46+
if (!parsed.success) return { error: 'Invalid input' };
47+
48+
const session = await auth();
49+
if (!session?.user) redirect('/login');
50+
51+
// Authorize: verify ownership, not just login
52+
await db.items.deleteMany({
53+
where: { id: parsed.data.id, userId: session.user.id }
54+
});
55+
}
56+
```
57+
58+
Every Server Action needs three things at the top:
59+
1. **Input validation** (Zod or similar runtime schema)
60+
2. **Authentication** (verify the user is logged in)
61+
3. **Authorization** (verify the user owns the resource)
62+
63+
## API Route Handlers
64+
65+
Same rules apply to `app/api/` route handlers. Every route handler is a public endpoint. Authenticate and authorize at the top of every handler.
66+
67+
## Data Leakage to Client Components
68+
69+
Never pass entire database objects to Client Components. They may contain sensitive fields (hashed passwords, internal IDs, admin flags). Select only the fields the client needs:
70+
71+
```typescript
72+
// BAD: leaks all fields to the client
73+
const user = await db.users.findUnique({ where: { id } });
74+
return <UserProfile user={user} />;
75+
76+
// GOOD: select only needed fields
77+
const user = await db.users.findUnique({
78+
where: { id },
79+
select: { name: true, avatarUrl: true }
80+
});
81+
return <UserProfile user={user} />;
82+
```
83+
84+
Use `import 'server-only'` at the top of data access modules to prevent them from being accidentally imported into Client Components.
85+
86+
## Session & Token Storage
87+
88+
- Store tokens in `HttpOnly + Secure + SameSite=Lax` cookies, **not localStorage**.
89+
- localStorage is accessible to any JavaScript on the page — a single XSS vulnerability exposes all tokens.
90+
- `HttpOnly` cookies are invisible to JavaScript and sent automatically by the browser.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Data Access & Input Validation
2+
3+
## SQL Injection
4+
5+
Always use parameterized queries or ORM methods. Never concatenate user input into SQL strings:
6+
7+
```typescript
8+
// BAD: SQL injection via string concatenation
9+
const result = await db.query(`SELECT * FROM users WHERE id = '${userId}'`);
10+
11+
// GOOD: parameterized query
12+
const result = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
13+
```
14+
15+
## ORM Safety (Prisma)
16+
17+
Even with an ORM, injection is possible:
18+
19+
- **Validate input types with Zod before passing to Prisma.** `findFirst` and similar methods are vulnerable to operator injection if unvalidated objects are passed as filter values. An attacker can send `{ "email": { "contains": "" } }` to match all records.
20+
21+
```typescript
22+
// BAD: raw request body passed directly to Prisma
23+
const user = await prisma.user.findFirst({ where: req.body });
24+
25+
// GOOD: validate with Zod first
26+
const schema = z.object({ email: z.string().email() });
27+
const parsed = schema.parse(req.body);
28+
const user = await prisma.user.findFirst({ where: { email: parsed.email } });
29+
```
30+
31+
- **Never use `$queryRawUnsafe` or `$executeRawUnsafe` with user-supplied input.** These bypass Prisma's parameterization entirely.
32+
33+
```typescript
34+
// BAD: raw SQL with user input
35+
const results = await prisma.$queryRawUnsafe(
36+
`SELECT * FROM users WHERE name = '${name}'`
37+
);
38+
39+
// GOOD: use the safe raw query with parameters
40+
const results = await prisma.$queryRaw`
41+
SELECT * FROM users WHERE name = ${name}
42+
`;
43+
```
44+
45+
## Input Validation
46+
47+
Validate all external input at system boundaries using a runtime schema validator (Zod, Yup, Joi, etc.):
48+
49+
- API route handlers
50+
- Server Actions
51+
- Webhook handlers
52+
- Form submissions
53+
- URL parameters and query strings
54+
55+
Don't rely on TypeScript types alone — they're compile-time only and don't exist at runtime. An attacker sending a malformed request bypasses all TypeScript checks.
56+
57+
```typescript
58+
// TypeScript type provides NO runtime protection
59+
type CreateUserInput = { name: string; email: string };
60+
61+
// Zod schema provides ACTUAL runtime validation
62+
const CreateUserSchema = z.object({
63+
name: z.string().min(1).max(100),
64+
email: z.string().email(),
65+
});
66+
```
67+
68+
## Mass Assignment
69+
70+
Don't spread request bodies directly into database operations. An attacker can add unexpected fields:
71+
72+
```typescript
73+
// BAD: attacker can add { isAdmin: true, credits: 99999 }
74+
await db.users.update({ where: { id }, data: req.body });
75+
76+
// GOOD: pick only allowed fields
77+
const { name, email } = validated.data;
78+
await db.users.update({ where: { id }, data: { name, email } });
79+
```

0 commit comments

Comments
 (0)