| 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