Files
gh-greyhaven-ai-claude-code…/skills/security-practices/SKILL.md
2025-11-29 18:29:28 +08:00

425 lines
10 KiB
Markdown

---
name: grey-haven-security-practices
description: Grey Haven's security best practices - input validation, output sanitization, multi-tenant RLS, secret management with Doppler, rate limiting, OWASP Top 10 for TanStack/FastAPI stack. Use when implementing security-critical features.
---
# Grey Haven Security Practices
Follow Grey Haven Studio's security best practices for TanStack Start and FastAPI applications.
## Secret Management with Doppler
**CRITICAL**: NEVER commit secrets to git. Always use Doppler.
### Doppler Setup
```bash
# Install Doppler CLI
brew install dopplerhq/cli/doppler
# Authenticate
doppler login
# Setup project
cd /path/to/project
doppler setup
# Access secrets
doppler run -- npm run dev # TypeScript
doppler run -- python app/main.py # Python
```
### Required Secrets (Doppler)
```bash
# Auth
BETTER_AUTH_SECRET=<random-32-bytes>
JWT_SECRET_KEY=<random-32-bytes>
# Database
DATABASE_URL_ADMIN=postgresql://...
DATABASE_URL_AUTHENTICATED=postgresql://...
# APIs
RESEND_API_KEY=re_...
STRIPE_SECRET_KEY=sk_...
OPENAI_API_KEY=sk-...
# OAuth
GOOGLE_CLIENT_SECRET=GOCSPX-...
GITHUB_CLIENT_SECRET=...
```
### Accessing Secrets in Code
```typescript
// [OK] Correct - Use process.env (Doppler provides at runtime)
const apiKey = process.env.OPENAI_API_KEY!;
// [X] Wrong - Hardcoded secrets
const apiKey = "sk-..."; // NEVER DO THIS!
```
```python
# [OK] Correct - Use os.getenv (Doppler provides at runtime)
import os
api_key = os.getenv("OPENAI_API_KEY")
# [X] Wrong - Hardcoded secrets
api_key = "sk-..." # NEVER DO THIS!
```
## Input Validation
### TypeScript (Zod Validation)
```typescript
import { z } from "zod";
// [OK] Validate all user input
const UserCreateSchema = z.object({
email_address: z.string().email().max(255),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150),
});
export const createUser = createServerFn("POST", async (data: unknown) => {
// Validate input
const validated = UserCreateSchema.parse(data);
// Now safe to use
await db.insert(users).values(validated);
});
```
### Python (Pydantic Validation)
```python
from pydantic import BaseModel, EmailStr, Field, validator
class UserCreate(BaseModel):
"""User creation schema with validation."""
email_address: EmailStr
name: str = Field(min_length=1, max_length=100)
age: int = Field(ge=0, le=150)
@validator("name")
def name_must_not_contain_special_chars(cls, v):
if not v.replace(" ", "").isalnum():
raise ValueError("Name must be alphanumeric")
return v
@router.post("/users", response_model=UserResponse)
async def create_user(data: UserCreate):
# Pydantic validates automatically
# data is now safe to use
pass
```
## Output Sanitization
### HTML Escaping (XSS Prevention)
```typescript
// [OK] React automatically escapes in JSX
function UserProfile({ user }: { user: User }) {
return <div>{user.name}</div>; // Safe, auto-escaped
}
// [X] Dangerous - Raw HTML
function UserProfile({ user }: { user: User }) {
return <div dangerouslySetInnerHTML={{ __html: user.bio }} />; // UNSAFE!
}
// [OK] If HTML needed, sanitize first
import DOMPurify from "isomorphic-dompurify";
function UserProfile({ user }: { user: User }) {
const sanitized = DOMPurify.sanitize(user.bio);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
```
### SQL Injection Prevention
```typescript
// [OK] Use parameterized queries (Drizzle)
const user = await db.query.users.findFirst({
where: eq(users.email_address, email), // Safe, parameterized
});
// [X] Never concatenate SQL
const user = await db.execute(
`SELECT * FROM users WHERE email = '${email}'` // SQL INJECTION!
);
```
```python
# [OK] Use ORM (SQLModel/SQLAlchemy)
user = await session.execute(
select(User).where(User.email_address == email) # Safe, parameterized
)
# [X] Never concatenate SQL
user = await session.execute(
f"SELECT * FROM users WHERE email = '{email}'" # SQL INJECTION!
)
```
## Multi-Tenant Security (RLS)
### Enable RLS on All Tables
```sql
-- ALWAYS enable RLS for multi-tenant tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE teams ENABLE ROW LEVEL SECURITY;
```
### Tenant Isolation Policies
```sql
-- Authenticated users see only their tenant's data
CREATE POLICY "Tenant isolation for users"
ON users FOR ALL TO authenticated
USING (tenant_id = (current_setting('request.jwt.claims')::json->>'tenant_id')::uuid);
```
### Always Include tenant_id in Queries
```typescript
// [OK] Correct - Tenant isolation enforced
export const getUser = createServerFn("GET", async (userId: string) => {
const session = await getSession();
const tenantId = session.user.tenant_id;
return await db.query.users.findFirst({
where: and(
eq(users.id, userId),
eq(users.tenant_id, tenantId) // REQUIRED!
),
});
});
// [X] Wrong - Missing tenant check (security vulnerability!)
export const getUser = createServerFn("GET", async (userId: string) => {
return await db.query.users.findFirst({
where: eq(users.id, userId), // Can access ANY tenant's users!
});
});
```
## Rate Limiting
### Redis-Based Rate Limiting
```typescript
import { Redis } from "@upstash/redis";
// Doppler provides REDIS_URL
const redis = new Redis({ url: process.env.REDIS_URL! });
async function rateLimit(identifier: string, limit: number, window: number) {
const key = `rate-limit:${identifier}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, window);
}
if (count > limit) {
throw new Error("Rate limit exceeded");
}
return { success: true, remaining: limit - count };
}
export const sendEmail = createServerFn("POST", async (data) => {
const session = await getSession();
// Rate limit: 10 emails per hour per user
await rateLimit(`email:${session.user.id}`, 10, 3600);
// Send email...
});
```
## Authentication Security
### Password Requirements
```typescript
const PasswordSchema = z.string()
.min(12, "Password must be at least 12 characters")
.regex(/[A-Z]/, "Must contain uppercase letter")
.regex(/[a-z]/, "Must contain lowercase letter")
.regex(/[0-9]/, "Must contain number")
.regex(/[^A-Za-z0-9]/, "Must contain special character");
```
### Session Security
```typescript
// lib/server/auth.ts
export const auth = betterAuth({
session: {
expiresIn: 7 * 24 * 60 * 60, // 7 days
updateAge: 24 * 60 * 60, // Refresh daily
cookieOptions: {
httpOnly: true, // Prevent XSS
secure: true, // HTTPS only
sameSite: "lax", // CSRF protection
},
},
});
```
## CORS Configuration
```typescript
// TanStack Start
import { cors } from "@elysiajs/cors";
app.use(cors({
origin: [
"https://app.greyhaven.studio",
"https://admin.greyhaven.studio",
],
credentials: true,
maxAge: 86400,
}));
```
```python
# FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://app.greyhaven.studio",
"https://admin.greyhaven.studio",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["*"],
max_age=86400,
)
```
## File Upload Security
```typescript
// Validate file type and size
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
export const uploadFile = createServerFn("POST", async (file: File) => {
// Validate size
if (file.size > MAX_FILE_SIZE) {
throw new Error("File too large");
}
// Validate type
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error("Invalid file type");
}
// Validate content (check file header, not just extension)
const buffer = await file.arrayBuffer();
const header = new Uint8Array(buffer.slice(0, 4));
// Check for valid image headers
// JPEG: FF D8 FF
// PNG: 89 50 4E 47
// etc.
// Generate safe filename (prevent path traversal)
const ext = file.name.split(".").pop();
const filename = `${crypto.randomUUID()}.${ext}`;
// Upload to secure storage...
});
```
## Environment-Specific Security
### Development
```bash
# Doppler: dev config
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
DEBUG=true
```
### Production
```bash
# Doppler: production config
BETTER_AUTH_URL=https://app.greyhaven.studio
CORS_ORIGINS=https://app.greyhaven.studio
DEBUG=false
FORCE_HTTPS=true
```
## Testing Security
```typescript
// tests/integration/security.test.ts
import { describe, it, expect } from "vitest";
describe("Security", () => {
it("prevents tenant data leakage", async () => {
// Create user in tenant A
const userA = await createUser({ email: "a@example.com", tenantId: "A" });
// Try to access as tenant B user
const sessionB = await loginAs({ tenantId: "B" });
const result = await getUserById(userA.id, sessionB);
// Should return null or 404, not tenant A's user
expect(result).toBeNull();
});
it("enforces rate limiting", async () => {
// Make 11 requests (limit is 10)
for (let i = 0; i < 11; i++) {
if (i < 10) {
await sendEmail({ to: "test@example.com" });
} else {
await expect(
sendEmail({ to: "test@example.com" })
).rejects.toThrow("Rate limit exceeded");
}
}
});
});
```
## When to Apply This Skill
Use this skill when:
- Handling user input
- Implementing authentication
- Working with sensitive data
- Configuring API endpoints
- Writing database queries
- Implementing file uploads
- Setting up CORS
- Managing secrets with Doppler
## Critical Reminders
1. **Doppler**: ALWAYS use for secrets (never commit to git)
2. **Input validation**: Validate ALL user input (Zod/Pydantic)
3. **RLS**: Enable on all multi-tenant tables
4. **tenant_id**: ALWAYS filter by tenant_id
5. **Rate limiting**: Implement on expensive operations
6. **HTTPS only**: Force HTTPS in production
7. **SQL injection**: Use ORM, never concatenate SQL
8. **XSS**: React auto-escapes, sanitize dangerouslySetInnerHTML
9. **CORS**: Whitelist specific origins
10. **Sessions**: httpOnly, secure, sameSite cookies