pinpoint-security
Security patterns, CSP nonces, input validation, auth checks, Supabase SSR patterns. Use when implementing authentication, forms, security features, or when user mentions security/validation/auth.
When & Why to Use This Skill
This Claude skill provides a comprehensive framework for implementing robust security patterns in modern web applications, specifically optimized for Next.js and Supabase SSR environments. It streamlines the integration of critical security measures including dynamic CSP nonces, Zod-based input validation, and secure authentication workflows to protect against XSS, CSRF, and unauthorized data access.
Use Cases
- Implementing secure authentication and authorization flows using Supabase SSR patterns and immediate session verification.
- Configuring dynamic Content Security Policy (CSP) with unique nonces via Next.js middleware to prevent cross-site scripting (XSS).
- Enforcing strict server-side input validation and sanitization using Zod to secure Server Actions and API endpoints.
- Auditing codebase for security anti-patterns such as hardcoded hostnames, weak CSP configurations, or improper authentication placement.
| name | pinpoint-security |
|---|---|
| description | Security patterns, CSP nonces, input validation, auth checks, Supabase SSR patterns. Use when implementing authentication, forms, security features, or when user mentions security/validation/auth. |
PinPoint Security Guide
When to Use This Skill
Use this skill when:
- Implementing authentication or authorization
- Creating forms or handling user input
- Setting up security headers (CSP, CORS, etc.)
- Working with Supabase SSR authentication
- User mentions: "security", "auth", "validation", "XSS", "CSRF", "input", "forms"
Quick Reference
Critical Security Rules
- CSP with nonces: Dynamic nonces via middleware, static headers via next.config.ts
- Validate ALL inputs: Use Zod for all form data and user inputs
- Supabase SSR contract: Use
~/lib/supabase/server, callauth.getUser()immediately - Host consistency: Use
localhostfor all auth callbacks, dev server, Playwright, Supabase site_url - No logic between
createClient()andgetUser()
Common Patterns
Server Action with Auth:
"use server";
import { createClient } from "~/lib/supabase/server";
import { redirect } from "next/navigation";
export async function protectedAction(formData: FormData) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser(); // Call immediately!
if (!user) redirect("/login");
// Validate inputs
const schema = z.object({
title: z.string().min(1),
description: z.string().optional(),
});
const validated = schema.parse({
title: formData.get("title"),
description: formData.get("description"),
});
// Proceed with validated data
}
Form Validation:
import { z } from "zod";
const createIssueSchema = z.object({
title: z.string().min(1, "Title required"),
description: z.string().optional(),
machineId: z.string().uuid("Invalid machine ID"),
severity: z.enum(["minor", "playable", "unplayable"]),
});
export async function createIssue(formData: FormData) {
const rawData = {
title: formData.get("title"),
description: formData.get("description"),
machineId: formData.get("machineId"),
severity: formData.get("severity"),
};
const validData = createIssueSchema.parse(rawData);
// Use validData safely
}
Detailed Documentation
For comprehensive security guidance, read the following documentation files:
# Security patterns and CSP configuration
cat docs/SECURITY.md
# Security-related non-negotiables
cat docs/NON_NEGOTIABLES.md | grep -A 20 "## Security"
Key Security Constraints (from NON_NEGOTIABLES.md)
CORE-SEC-001: Protect APIs and Server Actions
- Verify authentication in all Server Actions
- Check authorization before data access
- Never skip auth checks in protected routes
CORE-SEC-002: Validate all inputs
- Use Zod for all form data and user inputs
- Never trust FormData or query params without validation
- Prevent injection attacks (SQL, XSS, command injection)
CORE-SEC-003: Security headers via middleware
- CSP with nonces (prevents XSS)
- Set headers in
middleware.ts(dynamic) andnext.config.ts(static) - Don't remove or weaken Content-Security-Policy
CORE-SEC-004: Nonce-based CSP
- Generate unique nonce per request using Web Crypto API
- Use 'strict-dynamic' to allow Next.js dynamic imports
- Never use 'unsafe-inline' or 'unsafe-eval' in script-src
CORE-SEC-005: No hardcoded hostnames or ports
- Use
NEXT_PUBLIC_SITE_URLandPORTenvironment variables - Prevents environment mismatches and "whack-a-mole" bugs
CORE-SSR-001: Use SSR wrapper and cookie contract
- Use
~/lib/supabase/server'screateClient()with proper cookies - Don't import from
@supabase/supabase-jsdirectly on server
CORE-SSR-002: Call auth.getUser() immediately
- Workaround for timing issues
- Avoids token invalidation
- No logic between
createClient()andgetUser()
CORE-SSR-003: Middleware is required
- Enables token refresh and SSR session continuity
- Don't remove or bypass middleware
CORE-SSR-006: Database trigger for auto-profile creation
- OAuth-proof (works for Google/GitHub login)
- Atomic transaction via
handle_new_user()trigger - Don't create profiles manually in signup Server Actions
Code Examples
CSP with Nonces (Middleware)
See src/middleware.ts for implementation:
- Generates unique nonce per request
- Uses Web Crypto API (Edge Runtime compatible)
- Sets CSP header with nonce for scripts
- Passes nonce to Next.js pages via headers
Auth Protection Pattern
// Server Component
export default async function ProtectedPage() {
const supabase = await createClient();
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
redirect("/login");
}
return <DashboardContent user={user} />;
}
// Server Action
"use server";
export async function updateSetting(formData: FormData) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect("/login");
}
// Validate and process
}
Input Sanitization
import sanitizeHtml from "sanitize-html";
export async function createComment(formData: FormData) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
const rawContent = formData.get("content");
if (typeof rawContent !== "string") {
throw new Error("Invalid content");
}
// Sanitize HTML to prevent XSS
const sanitizedContent = sanitizeHtml(rawContent, {
allowedTags: ["b", "i", "em", "strong", "a", "p"],
allowedAttributes: {
a: ["href"],
},
});
await db.insert(comments).values({
userId: user.id,
content: sanitizedContent,
});
}
Security Checklist
Before deploying or merging security-related code:
- All Server Actions have auth checks (
auth.getUser()called immediately) - All form inputs validated with Zod
- No hardcoded
localhost:3000or specific ports (use env vars) - CSP nonces generated in middleware
- No 'unsafe-inline' or 'unsafe-eval' in CSP
- Supabase SSR wrapper used (
~/lib/supabase/server) - No logic between
createClient()andgetUser() - User-generated content sanitized before rendering
- OAuth callback route present at
app/auth/callback/route.ts - Database trigger
handle_new_user()exists for profile creation
Testing Security
Auth Protection Tests
// Test unauthorized access
it("redirects unauthenticated users", async () => {
// Mock unauthenticated session
const result = await protectedAction(formData);
expect(result).toRedirect("/login");
});
// Test authorized access
it("allows authenticated users", async () => {
// Mock authenticated session
const result = await protectedAction(formData);
expect(result).toSucceed();
});
Input Validation Tests
it("rejects invalid input", () => {
const invalidData = { title: "" }; // Empty title
expect(() => createIssueSchema.parse(invalidData)).toThrow();
});
it("accepts valid input", () => {
const validData = {
title: "Broken flipper",
machineId: "uuid-here",
severity: "playable",
};
expect(() => createIssueSchema.parse(validData)).not.toThrow();
});
Common Security Mistakes to Avoid
- Skipping auth checks: Every Server Action needs auth verification
- Trusting user input: Always validate with Zod
- Modifying Supabase response: Return response object as-is from middleware
- Wrong Supabase import: Use
~/lib/supabase/server, not direct imports - Logic before getUser(): Call
auth.getUser()immediately aftercreateClient() - Hardcoded URLs: Use environment variables for hosts/ports
- Weakening CSP: Don't add 'unsafe-inline' or remove nonce requirements
- Manual profile creation: Use database trigger, not signup Server Actions (OAuth won't work)
Additional Resources
- Security documentation:
docs/SECURITY.md - Non-negotiables:
docs/NON_NEGOTIABLES.md(CORE-SEC-* and CORE-SSR-* rules) - Supabase SSR guide: Use Context7 MCP for latest patterns