Back to Skills

grey-haven-security-practices

greyhaven-ai
Updated 2 days ago
17 views
15
2
15
View on GitHub
Testingapi

About

This Claude Skill provides Grey Haven's security best practices for TanStack Start and FastAPI applications. It covers critical security measures including input validation, output sanitization, multi-tenant RLS, secret management with Doppler, rate limiting, and OWASP Top 10 compliance. Use this skill when implementing security-critical features to ensure robust application security.

Quick Install

Claude Code

Recommended
Plugin CommandRecommended
/plugin add https://github.com/greyhaven-ai/claude-code-config
Git CloneAlternative
git clone https://github.com/greyhaven-ai/claude-code-config.git ~/.claude/skills/grey-haven-security-practices

Copy and paste this command in Claude Code to install this skill

Documentation

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

# 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)

# 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

// [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!
# [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)

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)

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)

// [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

// [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!
);
# [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

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

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

// [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

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

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

// 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

// TanStack Start
import { cors } from "@elysiajs/cors";

app.use(cors({
  origin: [
    "https://app.greyhaven.studio",
    "https://admin.greyhaven.studio",
  ],
  credentials: true,
  maxAge: 86400,
}));
# 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

// 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

# Doppler: dev config
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
DEBUG=true

Production

# Doppler: production config
BETTER_AUTH_URL=https://app.greyhaven.studio
CORS_ORIGINS=https://app.greyhaven.studio
DEBUG=false
FORCE_HTTPS=true

Testing Security

// 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: "[email protected]", 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: "[email protected]" });
      } else {
        await expect(
          sendEmail({ to: "[email protected]" })
        ).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

GitHub Repository

greyhaven-ai/claude-code-config
Path: grey-haven-plugins/security/skills/security-practices

Related Skills

evaluating-llms-harness

Testing

This Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.

View skill

langchain

Meta

LangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.

View skill

creating-opencode-plugins

Meta

This skill provides the structure and API specifications for creating OpenCode plugins that hook into 25+ event types like commands, files, and LSP operations. It offers implementation patterns for JavaScript/TypeScript modules that intercept and extend the AI assistant's lifecycle. Use it when you need to build event-driven plugins for monitoring, custom handling, or extending OpenCode's capabilities.

View skill

nestjs

Meta

This skill provides NestJS development standards and architectural patterns for building domain-centric applications. It covers modular design, dependency injection, decorator patterns, and key framework features like controllers, services, middleware, and interceptors. Use it when developing NestJS applications, implementing APIs, configuring microservices, or integrating with databases.

View skill