agentsclimarketplace

Security headers

Skill PIXARTSeu/Synapse/packages/codegraph/data/skill/security-headers

Self-improving AI brain for Claude Code & Desktop — 28 MCP tools, 253 skills, collective memory, project tracking, work logs. One server, all your sessions share the same knowledge. Deploy on Coolify in 2 minutes.

Install
npx -y skills add PIXARTSeu/Synapse --skill security-headers

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Security headers and hardening for Next.js — CSP, CORS, rate limiting, CSRF protection, input sanitization, secrets management. Use when hardening a Next.js app, configuring security headers, or implementing rate limiting.

SKILL.md

21.8 KB, ~5.6k tokens by cl100k_base, as published. Nobody here has run it

Security Headers & Hardening for Next.js

1. Security Headers in next.config.js

Complete Headers Configuration

// next.config.ts
import type { NextConfig } from "next";

const securityHeaders = [
  // Prevent clickjacking
  {
    key: "X-Frame-Options",
    value: "DENY", // or SAMEORIGIN if you embed your own iframes
  },
  // Prevent MIME type sniffing
  {
    key: "X-Content-Type-Options",
    value: "nosniff",
  },
  // HSTS — force HTTPS for 2 years, include subdomains, allow preload list
  {
    key: "Strict-Transport-Security",
    value: "max-age=63072000; includeSubDomains; preload",
  },
  // Control referrer information
  {
    key: "Referrer-Policy",
    value: "strict-origin-when-cross-origin",
  },
  // Disable browser features you don't use
  {
    key: "Permissions-Policy",
    value: "camera=(), microphone=(), geolocation=(), interest-cohort=()",
  },
  // Prevent XSS in older browsers (modern browsers use CSP)
  {
    key: "X-XSS-Protection",
    value: "1; mode=block",
  },
  // DNS prefetch control
  {
    key: "X-DNS-Prefetch-Control",
    value: "on",
  },
];

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        // Apply to all routes
        source: "/(.*)",
        headers: securityHeaders,
      },
    ];
  },
};

export default nextConfig;

CSP Header (Static — No Nonce)

For sites without inline scripts, add CSP directly in next.config.ts:

const cspHeader = `
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' blob: data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  frame-src 'none';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
`.replace(/\n/g, "");

// Add to securityHeaders array:
{ key: "Content-Security-Policy", value: cspHeader }

2. CSP with Nonce (App Router)

For apps that need inline scripts (analytics, etc.), use nonce-based CSP via middleware.

middleware.ts — Generate Nonce

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // Generate a random nonce for each request
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");

  // Build CSP with the nonce
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data: https:;
    font-src 'self';
    connect-src 'self';
    frame-src 'none';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
  `.replace(/\s{2,}/g, " ").trim();

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  });

  // Use Report-Only during testing, then switch to enforcing
  response.headers.set("Content-Security-Policy", cspHeader);

  return response;
}

export const config = {
  matcher: [
    // Skip static files and images
    "/((?!_next/static|_next/image|favicon.ico).*)",
  ],
};

Read Nonce in Layout

// app/layout.tsx
import { headers } from "next/headers";
import Script from "next/script";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const headersList = await headers();
  const nonce = headersList.get("x-nonce") ?? "";

  return (
    <html lang="en">
      <body>
        {children}
        {/* Analytics script with nonce */}
        <Script
          src="https://analytics.example.com/script.js"
          nonce={nonce}
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

Report-Only Mode for Testing

Switch to report-only before enforcing. This logs violations without blocking:

// In middleware.ts, replace:
response.headers.set("Content-Security-Policy", cspHeader);
// With:
response.headers.set("Content-Security-Policy-Report-Only", cspHeader);

CSP Report Endpoint

// app/api/csp-report/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const report = body["csp-report"] || body;

    console.warn("[CSP Violation]", {
      blockedUri: report["blocked-uri"],
      violatedDirective: report["violated-directive"],
      documentUri: report["document-uri"],
      sourceFile: report["source-file"],
      lineNumber: report["line-number"],
    });

    // Send to your logging service (Sentry, LogDrain, etc.)
    // await logToService("csp-violation", report);

    return NextResponse.json({ received: true });
  } catch {
    return NextResponse.json({ error: "Invalid report" }, { status: 400 });
  }
}

Add report-uri /api/csp-report; to your CSP header to enable reporting.


3. CORS Configuration

API Route CORS Handler

// lib/cors.ts
const ALLOWED_ORIGINS: Record<string, string[]> = {
  development: ["http://localhost:3000", "http://localhost:3001"],
  production: ["https://example.com", "https://app.example.com"],
};

export function getCorsHeaders(origin: string | null) {
  const env = process.env.NODE_ENV || "development";
  const origins = ALLOWED_ORIGINS[env] ?? ALLOWED_ORIGINS.production;

  const isAllowed = origin && origins.includes(origin);

  return {
    "Access-Control-Allow-Origin": isAllowed ? origin : "",
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization, X-CSRF-Token",
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Max-Age": "86400",
  };
}

API Route with CORS

// app/api/data/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/cors";

// Handle preflight
export async function OPTIONS(request: NextRequest) {
  const origin = request.headers.get("origin");
  return new NextResponse(null, {
    status: 204,
    headers: getCorsHeaders(origin),
  });
}

export async function GET(request: NextRequest) {
  const origin = request.headers.get("origin");
  const headers = getCorsHeaders(origin);

  if (!headers["Access-Control-Allow-Origin"]) {
    return NextResponse.json({ error: "Origin not allowed" }, { status: 403 });
  }

  const data = { message: "Hello" };
  return NextResponse.json(data, { headers });
}

CORS via Middleware (All API Routes)

// middleware.ts — add CORS handling alongside CSP
import { NextResponse, type NextRequest } from "next/server";
import { getCorsHeaders } from "@/lib/cors";

export function middleware(request: NextRequest) {
  // CORS for /api/* routes
  if (request.nextUrl.pathname.startsWith("/api/")) {
    const origin = request.headers.get("origin");

    // Handle preflight
    if (request.method === "OPTIONS") {
      return new NextResponse(null, {
        status: 204,
        headers: getCorsHeaders(origin),
      });
    }

    const response = NextResponse.next();
    const corsHeaders = getCorsHeaders(origin);
    Object.entries(corsHeaders).forEach(([key, value]) => {
      response.headers.set(key, value);
    });
    return response;
  }

  // CSP for pages (see section 2)
  // ...
}

4. Rate Limiting

In-Memory Rate Limiter (Single Instance)

// lib/rate-limit.ts
interface RateLimitEntry {
  count: number;
  resetAt: number;
}

const store = new Map<string, RateLimitEntry>();

// Clean expired entries every 60s
setInterval(() => {
  const now = Date.now();
  for (const [key, entry] of store) {
    if (entry.resetAt < now) store.delete(key);
  }
}, 60_000);

export function rateLimit(
  key: string,
  limit: number = 10,
  windowMs: number = 60_000
): { success: boolean; remaining: number; resetAt: number } {
  const now = Date.now();
  const entry = store.get(key);

  if (!entry || entry.resetAt < now) {
    store.set(key, { count: 1, resetAt: now + windowMs });
    return { success: true, remaining: limit - 1, resetAt: now + windowMs };
  }

  if (entry.count >= limit) {
    return { success: false, remaining: 0, resetAt: entry.resetAt };
  }

  entry.count++;
  return { success: true, remaining: limit - entry.count, resetAt: entry.resetAt };
}

Redis-Based Rate Limiter (Upstash)

// lib/rate-limit-redis.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

// Sliding window: 10 requests per 10 seconds
export const rateLimiter = new Ratelimit({
  redis: Redis.fromEnv(), // Uses UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN
  limiter: Ratelimit.slidingWindow(10, "10 s"),
  analytics: true,
  prefix: "@upstash/ratelimit",
});

// Stricter limiter for auth endpoints
export const authRateLimiter = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, "60 s"),
  analytics: true,
  prefix: "@upstash/ratelimit:auth",
});

Apply in Middleware

// middleware.ts — rate limiting section
import { NextResponse, type NextRequest } from "next/server";
import { rateLimiter, authRateLimiter } from "@/lib/rate-limit-redis";

async function applyRateLimit(request: NextRequest): Promise<NextResponse | null> {
  // Get identifier: prefer authenticated user ID, fallback to IP
  const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
    ?? request.headers.get("x-real-ip")
    ?? "anonymous";

  const identifier = ip;
  const isAuthRoute = request.nextUrl.pathname.startsWith("/api/auth");
  const limiter = isAuthRoute ? authRateLimiter : rateLimiter;

  const { success, limit, remaining, reset } = await limiter.limit(identifier);

  if (!success) {
    return NextResponse.json(
      { error: "Too many requests. Please try again later." },
      {
        status: 429,
        headers: {
          "X-RateLimit-Limit": limit.toString(),
          "X-RateLimit-Remaining": remaining.toString(),
          "X-RateLimit-Reset": reset.toString(),
          "Retry-After": Math.ceil((reset - Date.now()) / 1000).toString(),
        },
      }
    );
  }

  return null; // Continue processing
}

export async function middleware(request: NextRequest) {
  // Rate limit API routes
  if (request.nextUrl.pathname.startsWith("/api/")) {
    const rateLimitResponse = await applyRateLimit(request);
    if (rateLimitResponse) return rateLimitResponse;
  }

  return NextResponse.next();
}

Per-User Rate Limiting (After Auth)

// lib/rate-limit-user.ts
import { auth } from "@/auth";
import { rateLimiter } from "@/lib/rate-limit-redis";

export async function rateLimitByUser(fallbackIp: string) {
  const session = await auth();
  const identifier = session?.user?.id ?? `ip:${fallbackIp}`;
  return rateLimiter.limit(identifier);
}

5. CSRF Protection

Next.js Server Actions (Built-In)

Next.js 15 Server Actions have built-in CSRF protection. The framework automatically:

  • Validates the Origin header matches the host
  • Uses a non-guessable action ID

No extra work needed for Server Actions. Use them over raw API routes when possible.

Double Submit Cookie for API Routes

// lib/csrf.ts
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";

const CSRF_COOKIE = "__csrf";
const CSRF_HEADER = "x-csrf-token";

export function generateCsrfToken(): string {
  return crypto.randomUUID();
}

export async function setCsrfCookie(): Promise<string> {
  const token = generateCsrfToken();
  const cookieStore = await cookies();
  cookieStore.set(CSRF_COOKIE, token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
    path: "/",
    maxAge: 60 * 60, // 1 hour
  });
  return token;
}

export async function validateCsrf(request: NextRequest): Promise<boolean> {
  const cookieToken = request.cookies.get(CSRF_COOKIE)?.value;
  const headerToken = request.headers.get(CSRF_HEADER);

  if (!cookieToken || !headerToken) return false;
  return cookieToken === headerToken;
}

CSRF Token API + Validated Route

// app/api/csrf/route.ts — issue token
import { NextResponse } from "next/server";
import { setCsrfCookie } from "@/lib/csrf";

export async function GET() {
  const token = await setCsrfCookie();
  return NextResponse.json({ csrfToken: token });
}
// app/api/protected/route.ts — validate token
import { NextRequest, NextResponse } from "next/server";
import { validateCsrf } from "@/lib/csrf";

export async function POST(request: NextRequest) {
  const valid = await validateCsrf(request);
  if (!valid) {
    return NextResponse.json({ error: "Invalid CSRF token" }, { status: 403 });
  }

  // Process the request safely
  const data = await request.json();
  return NextResponse.json({ success: true });
}

Client-Side Usage

// hooks/useCsrf.ts
"use client";
import { useEffect, useState } from "react";

export function useCsrf() {
  const [token, setToken] = useState<string>("");

  useEffect(() => {
    fetch("/api/csrf")
      .then((r) => r.json())
      .then((d) => setToken(d.csrfToken));
  }, []);

  return token;
}

// Usage in a form submission:
const csrfToken = useCsrf();

await fetch("/api/protected", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-csrf-token": csrfToken,
  },
  body: JSON.stringify({ data: "value" }),
});

6. Input Sanitization

Zod Validation at API Boundary

Always validate ALL input with Zod before processing:

// lib/validations/contact.ts
import { z } from "zod";

export const contactSchema = z.object({
  name: z
    .string()
    .min(1, "Name is required")
    .max(100, "Name too long")
    .regex(/^[\p{L}\p{N}\s\-'.]+$/u, "Invalid characters in name"),
  email: z
    .string()
    .email("Invalid email")
    .max(254, "Email too long")
    .toLowerCase(),
  message: z
    .string()
    .min(10, "Message too short")
    .max(5000, "Message too long")
    .trim(),
  // Prevent hidden fields injection
  honeypot: z.string().max(0, "Bot detected").optional(),
});

export type ContactInput = z.infer<typeof contactSchema>;
// app/api/contact/route.ts
import { NextRequest, NextResponse } from "next/server";
import { contactSchema } from "@/lib/validations/contact";

export async function POST(request: NextRequest) {
  const body = await request.json();
  const result = contactSchema.safeParse(body);

  if (!result.success) {
    return NextResponse.json(
      { errors: result.error.flatten().fieldErrors },
      { status: 400 }
    );
  }

  const { name, email, message } = result.data;
  // Safe to use — validated and sanitized by Zod
}

DOMPurify for User HTML

// lib/sanitize.ts
import DOMPurify from "isomorphic-dompurify";

export function sanitizeHtml(dirty: string): string {
  return DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br", "ul", "ol", "li"],
    ALLOWED_ATTR: ["href", "target", "rel"],
    ALLOW_DATA_ATTR: false,
  });
}

// Usage in a component
function UserContent({ html }: { html: string }) {
  const clean = sanitizeHtml(html);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

SQL Injection Prevention

Always use parameterized queries. Never interpolate user input into SQL:

// WRONG — vulnerable to SQL injection
const user = await db.query(`SELECT * FROM users WHERE id = '${userId}'`);

// CORRECT — Prisma (parameterized by default)
const user = await prisma.user.findUnique({ where: { id: userId } });

// CORRECT — Drizzle (parameterized by default)
const user = await db.select().from(users).where(eq(users.id, userId));

// CORRECT — Raw SQL with parameters
const user = await db.execute(sql`SELECT * FROM users WHERE id = ${userId}`);

Path Traversal Prevention

// lib/safe-path.ts
import path from "path";

const UPLOAD_DIR = path.resolve(process.cwd(), "uploads");

export function safePath(userInput: string): string | null {
  // Resolve the full path
  const resolved = path.resolve(UPLOAD_DIR, userInput);

  // Ensure it stays within the allowed directory
  if (!resolved.startsWith(UPLOAD_DIR)) {
    return null; // Path traversal attempt
  }

  // Block dangerous patterns
  if (/\.\.|~|%2e%2e|%00/i.test(userInput)) {
    return null;
  }

  return resolved;
}

// Usage
export async function GET(request: NextRequest) {
  const filename = request.nextUrl.searchParams.get("file");
  if (!filename) return NextResponse.json({ error: "Missing file" }, { status: 400 });

  const filePath = safePath(filename);
  if (!filePath) {
    return NextResponse.json({ error: "Invalid file path" }, { status: 403 });
  }

  // Safe to read the file
}

7. Secrets Management

.env.local Best Practices

# .env.local — NEVER commit this file
# Add to .gitignore: .env*.local

# Server-only secrets (safe — never sent to browser)
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
AUTH_SECRET="randomly-generated-64-char-string"
STRIPE_SECRET_KEY="sk_live_..."
RESEND_API_KEY="re_..."

# Public vars (exposed to browser — only non-sensitive values)
NEXT_PUBLIC_APP_URL="https://example.com"
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."

Runtime vs Build-Time Env Vars

// lib/env.ts — validate env at startup with Zod
import { z } from "zod";

// Server-only env (validated at build/startup)
const serverSchema = z.object({
  DATABASE_URL: z.string().url(),
  AUTH_SECRET: z.string().min(32),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
  NODE_ENV: z.enum(["development", "production", "test"]),
});

// Client env (available in browser — no secrets here)
const clientSchema = z.object({
  NEXT_PUBLIC_APP_URL: z.string().url(),
  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
});

export const serverEnv = serverSchema.parse(process.env);
export const clientEnv = clientSchema.parse({
  NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
});

NEXT_PUBLIC_ Exposure Risks

RULE: Never prefix secrets with NEXT_PUBLIC_

NEXT_PUBLIC_* vars are inlined into the client JS bundle at build time.
They are visible to anyone who views your page source.

SAFE:     NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_GA_ID
DANGER:   NEXT_PUBLIC_API_SECRET, NEXT_PUBLIC_DB_URL

If you need a secret client-side, create a server API route that uses the secret and call it from the client.

Vault / Infisical Integration Pattern

// lib/secrets.ts — fetch secrets from Infisical at startup
import { InfisicalClient } from "@infisical/sdk";

let cachedSecrets: Record<string, string> | null = null;

export async function getSecrets(): Promise<Record<string, string>> {
  if (cachedSecrets) return cachedSecrets;

  const client = new InfisicalClient({
    siteUrl: "https://app.infisical.com",
  });

  await client.auth().universalAuth.login({
    clientId: process.env.INFISICAL_CLIENT_ID!,
    clientSecret: process.env.INFISICAL_CLIENT_SECRET!,
  });

  const secrets = await client.secrets().listSecrets({
    environment: process.env.NODE_ENV === "production" ? "prod" : "dev",
    projectId: process.env.INFISICAL_PROJECT_ID!,
  });

  cachedSecrets = Object.fromEntries(
    secrets.map((s) => [s.secretKey, s.secretValue])
  );

  return cachedSecrets;
}

// Usage in server code
const secrets = await getSecrets();
const dbUrl = secrets.DATABASE_URL;

Secret Rotation Strategy

1. Use Infisical/Vault with automatic rotation policies
2. Never hardcode secrets — always use env vars or secret manager
3. Rotate immediately if a secret is exposed in git history:
   - Revoke the old secret
   - Generate a new one
   - Update in secret manager
   - Redeploy
4. Use short-lived tokens where possible (JWT with 15min expiry)
5. Audit secret access logs monthly

8. OWASP Top 10 Checklist for Next.js

#OWASP RiskNext.js Mitigation
A01Broken Access ControlMiddleware auth checks, Server Actions with auth(), route-level guards
A02Cryptographic FailuresHTTPS via HSTS header, secure cookies (httpOnly, secure, sameSite)
A03InjectionZod validation, parameterized queries (Prisma/Drizzle), DOMPurify
A04Insecure DesignServer Components by default (no client data exposure), RSC boundaries
A05Security MisconfigurationSecurity headers (CSP, X-Frame, HSTS), env validation at startup
A06Vulnerable Componentsnpm audit, Dependabot/Renovate, lock file pinning
A07Auth FailuresAuth.js v5, rate limiting on login, CSRF protection, session rotation
A08Data Integrity FailuresCSP with nonce (prevent script injection), SRI for external scripts
A09Logging & MonitoringCSP report endpoint, Sentry error tracking, structured logging
A10SSRFValidate/allowlist URLs in Server Components, block internal IPs in fetches

Quick Hardening Checklist

[ ] Security headers configured in next.config.ts or middleware
[ ] CSP with nonce for inline scripts
[ ] HSTS enabled with preload
[ ] Rate limiting on all API routes (stricter on auth)
[ ] CSRF protection on state-changing endpoints
[ ] All user input validated with Zod
[ ] No secrets in NEXT_PUBLIC_* vars
[ ] .env*.local in .gitignore
[ ] npm audit clean (no critical/high vulnerabilities)
[ ] Cookies: httpOnly + secure + sameSite=strict
[ ] Error pages don't leak stack traces in production
[ ] File uploads validated (type, size, path traversal)
[ ] CORS allowlist per environment
[ ] Logging for security events (failed auth, rate limits, CSP violations)

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 327,132. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.