
Security Nextjs
- 308 installs
- 125 repo stars
- Updated February 4, 2026
- igorwarzocha/opencode-workflows
security-nextjs is a security agent skill that audits and hardens Next.js applications for common vulnerabilities including authentication, headers, SSR data leaks, environment secrets, and route exposure before producti
About
security-nextjs is an opencode-workflows skill for developers shipping Next.js applications who need a structured pre-launch security review. The skill focuses on App Router and Pages Router risks such as misconfigured auth boundaries, missing security headers, accidental SSR data leaks to the client, exposed environment variables, and unintended public API or server action routes. Agents using the skill walk repositories for Next.js-specific patterns—middleware, `getServerSideProps`, server components, and API routes—and produce remediation guidance aligned with production launch gates. Developers invoke it when a Next.js app is feature-complete but not yet hardened for public traffic or compliance review. It complements generic OWASP checklists by anchoring checks to Next.js file conventions, edge middleware, and deployment environments like Vercel or Node hosting.
- Next.js-specific threat coverage
- Auth and SSR leak checks
- Security header guidance
- Pre-launch hardening workflow
- OpenCode security automation
Security Nextjs by the numbers
- 308 all-time installs (skills.sh)
- +12 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #629 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/igorwarzocha/opencode-workflows --skill security-nextjsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 308 |
|---|---|
| repo stars | ★ 125 |
| Last updated | February 4, 2026 |
| Repository | igorwarzocha/opencode-workflows ↗ |
How do you audit Next.js apps for production security?
Audit and harden Next.js apps for common vulnerabilities—auth, headers, SSR data leaks, env secrets, and route exposure—before production launch.
Who is it for?
Next.js teams preparing a production launch who need an App Router–aware security pass beyond generic lint rules.
Skip if: Non-Next.js frameworks, pure infrastructure pentests, or apps still in early scaffolding without routes to review.
When should I use this skill?
A Next.js application is nearing launch and needs review of auth, headers, SSR data handling, secrets, and route exposure.
What you get
Security findings list, hardened middleware and headers config, env secret remediation, and route exposure report.
- security findings report
- remediation checklist
Files
<overview>
Security audit patterns for Next.js applications covering environment variable exposure, Server Actions, middleware auth, API routes, and App Router security.
</overview>
<rules>
Environment Variable Exposure
The NEXT_PUBLIC_ Footgun
NEXT_PUBLIC_* → Bundled into client JavaScript → Visible to everyone
No prefix → Server-only → Safe for secretsAudit steps: 1. grep -r "NEXT_PUBLIC_" . -g "*.env*" 2. For each var, ask: "Would I be OK if this was in view-source?" 3. Common mistakes:
NEXT_PUBLIC_API_KEY(SHOULD be server-only)NEXT_PUBLIC_DATABASE_URL(MUST NOT use)NEXT_PUBLIC_STRIPE_SECRET_KEY(useSTRIPE_SECRET_KEY)
Safe pattern:
// Server-only (API route, Server Component, Server Action)
const apiKey = process.env.API_KEY; // ✓ No NEXT_PUBLIC_
// Client-safe (truly public)
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY; // ✓ Publishablenext.config.js env Is Always Bundled
Values set in next.config.js under env are inlined into the client bundle, even without NEXT_PUBLIC_. Treat them as public.
// ❌ Sensitive values here are exposed to the browser
module.exports = {
env: {
DATABASE_URL: process.env.DATABASE_URL,
},
};</rules>
<vulnerabilities>
Server Actions Security
Missing Auth (Most Common Issue)
// ❌ VULNERABLE: No auth check
"use server"
export async function deleteUser(userId: string) {
await db.user.delete({ where: { id: userId } });
}
// ✓ SECURE: Auth + authorization
"use server"
export async function deleteUser(userId: string) {
const session = await getServerSession();
if (!session) throw new Error("Unauthorized");
if (session.user.id !== userId && !session.user.isAdmin) {
throw new Error("Forbidden");
}
await db.user.delete({ where: { id: userId } });
}Input Validation
// ❌ Trusts client input
"use server"
export async function updateProfile(data: any) {
await db.user.update({ data });
}
// ✓ Validates with Zod
"use server"
import { z } from "zod";
const schema = z.object({ name: z.string().max(100), bio: z.string().max(500) });
export async function updateProfile(formData: FormData) {
const data = schema.parse(Object.fromEntries(formData));
await db.user.update({ data });
}API Routes Security
App Router (app/api/*/route.ts)
// ❌ No auth
export async function GET(request: Request) {
return Response.json(await db.users.findMany());
}
// ✓ Auth middleware
import { getServerSession } from "next-auth";
export async function GET(request: Request) {
const session = await getServerSession();
if (!session) return new Response("Unauthorized", { status: 401 });
// ...
}Pages Router (pages/api/*.ts)
// Check for missing auth on all handlers
// Common issue: GET is public but POST has auth (inconsistent)Middleware Security
Auth in middleware.ts
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("session");
// ❌ Just checking existence
if (!token) return NextResponse.redirect("/login");
// ✓ SHOULD verify token
// But middleware can't do async DB calls easily!
// Solution: Use next-auth middleware or verify JWT
}
// CRITICAL: Check matcher covers all protected routes
export const config = {
matcher: ["/dashboard/:path*", "/admin/:path*", "/api/admin/:path*"],
};Matcher Gaps
// ❌ Forgot API routes
matcher: ["/dashboard/:path*"]
// Admin API at /api/admin/* is unprotected!
// ✓ Include API routes
matcher: ["/dashboard/:path*", "/api/admin/:path*"]Headers & Security Config
next.config.js
// Check for security headers
module.exports = {
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
// CSP is complex - check if present and not too permissive
],
},
];
},
};</vulnerabilities>
<severity_table>
Common Vulnerabilities
| Issue | Where to Look | Severity |
|---|---|---|
| NEXT_PUBLIC_ secrets | .env* files | CRITICAL |
| Unauth'd Server Actions | app/**/actions.ts | HIGH |
| Unauth'd API routes | app/api/**/route.ts, pages/api/** | HIGH |
| Middleware matcher gaps | middleware.ts | HIGH |
| Missing input validation | Server Actions, API routes | HIGH |
| IDOR in dynamic routes | [id] params without ownership check | HIGH |
| dangerouslySetInnerHTML | Components | MEDIUM |
| Missing security headers | next.config.js | LOW |
</severity_table>
<commands>
Quick Grep Commands
# Find NEXT_PUBLIC_ usage
grep -r "NEXT_PUBLIC_" . -g "*.env*" -g "*.ts" -g "*.tsx"
# Find next.config env usage (always bundled)
rg -n 'env\s*:' next.config.*
# Find Server Actions without auth
rg -l '"use server"' . | xargs rg -L '(getServerSession|auth\(|getSession|currentUser)'
# Find API routes
fd 'route\.(ts|js)' app/api/
# Find dangerouslySetInnerHTML
rg 'dangerouslySetInnerHTML' . -g "*.tsx" -g "*.jsx"</commands>
#!/usr/bin/env bash
# Next.js Security Scanner - First-pass automated detection
# Usage: ./scan.sh [directory]
set -euo pipefail
DIR="${1:-.}"
FOUND=0
echo "=== NEXT.JS SECURITY SCAN ==="
echo "Directory: $DIR"
echo "Timestamp: $(date -Iseconds)"
echo ""
if ! command -v rg &> /dev/null; then
echo "[ERROR] ripgrep (rg) required"
exit 1
fi
report() {
local severity="$1"
local title="$2"
local file="$3"
local line="${4:-}"
echo "[$severity] $title"
if [[ -n "$line" ]]; then
echo " File: $file:$line"
else
echo " File: $file"
fi
echo ""
FOUND=$((FOUND + 1))
}
echo "=== CRITICAL: NEXT_PUBLIC_ Secrets ==="
echo ""
# Check .env files for suspicious NEXT_PUBLIC_ vars
for envfile in $(find "$DIR" -name ".env*" -type f 2>/dev/null); do
while IFS=: read -r line_num content; do
[[ -z "$content" ]] && continue
# Check for suspicious patterns
if echo "$content" | grep -qiE 'NEXT_PUBLIC_.*(SECRET|KEY|PASSWORD|TOKEN|PRIVATE)'; then
report "CRITICAL" "Suspicious NEXT_PUBLIC_ variable (secrets exposed to client)" "$envfile" "$line_num"
fi
done < <(grep -n 'NEXT_PUBLIC_' "$envfile" 2>/dev/null || true)
done
echo "=== HIGH: next.config env Exposure ==="
echo ""
NEXT_CONFIG_FILE="$DIR/next.config.js"
[[ ! -f "$NEXT_CONFIG_FILE" ]] && NEXT_CONFIG_FILE="$DIR/next.config.mjs"
[[ ! -f "$NEXT_CONFIG_FILE" ]] && NEXT_CONFIG_FILE="$DIR/next.config.ts"
if [[ -f "$NEXT_CONFIG_FILE" ]]; then
if rg -q 'env\s*:' "$NEXT_CONFIG_FILE" 2>/dev/null; then
if rg -q '(SECRET|KEY|PASSWORD|TOKEN|PRIVATE)' "$NEXT_CONFIG_FILE" 2>/dev/null; then
report "HIGH" "next.config env contains sensitive-looking keys (values are bundled)" "$NEXT_CONFIG_FILE"
else
report "MEDIUM" "next.config env detected (values are bundled to client)" "$NEXT_CONFIG_FILE"
fi
fi
fi
echo "=== HIGH: Unauthenticated Server Actions ==="
echo ""
# Find server action files
ACTION_FILES=$(rg -l '"use server"' "$DIR" -g "*.ts" -g "*.tsx" 2>/dev/null || true)
for file in $ACTION_FILES; do
[[ -z "$file" ]] && continue
# Check if file has auth
if ! rg -q '(getServerSession|auth\(|getSession|currentUser|getAuthUserId)' "$file" 2>/dev/null; then
report "HIGH" "Server Action file without auth checks" "$file"
fi
done
echo "=== HIGH: Unauthenticated API Routes ==="
echo ""
# Find API route files (App Router)
API_ROUTES=$(find "$DIR" -path "*/app/api/*" -name "route.ts" -o -path "*/app/api/*" -name "route.js" 2>/dev/null || true)
for file in $API_ROUTES; do
[[ -z "$file" ]] && continue
if ! rg -q '(getServerSession|auth\(|getSession|NextAuth)' "$file" 2>/dev/null; then
report "HIGH" "API route without apparent auth" "$file"
fi
done
echo "=== MEDIUM: Middleware Matcher Gaps ==="
echo ""
MIDDLEWARE_FILE="$DIR/middleware.ts"
if [[ -f "$MIDDLEWARE_FILE" ]]; then
# Check if matcher exists
if ! rg -q 'matcher' "$MIDDLEWARE_FILE" 2>/dev/null; then
report "MEDIUM" "Middleware without matcher config (applies to all routes)" "$MIDDLEWARE_FILE"
fi
# Check if /api routes are in matcher
if rg -q 'matcher' "$MIDDLEWARE_FILE" && ! rg -q '/api' "$MIDDLEWARE_FILE"; then
report "MEDIUM" "Middleware matcher may not cover /api routes" "$MIDDLEWARE_FILE"
fi
fi
echo "=== MEDIUM: dangerouslySetInnerHTML ==="
echo ""
while IFS=: read -r file line match; do
[[ -z "$file" ]] && continue
report "MEDIUM" "dangerouslySetInnerHTML usage (XSS risk if unsanitized)" "$file" "$line"
done < <(rg -n --no-heading 'dangerouslySetInnerHTML' "$DIR" -g "*.tsx" -g "*.jsx" 2>/dev/null || true)
echo "=== INFO: Security Headers ==="
echo ""
NEXT_CONFIG="$DIR/next.config.js"
[[ ! -f "$NEXT_CONFIG" ]] && NEXT_CONFIG="$DIR/next.config.mjs"
[[ ! -f "$NEXT_CONFIG" ]] && NEXT_CONFIG="$DIR/next.config.ts"
if [[ -f "$NEXT_CONFIG" ]]; then
if ! rg -q 'headers' "$NEXT_CONFIG" 2>/dev/null; then
report "LOW" "No security headers configured in next.config" "$NEXT_CONFIG"
fi
fi
echo "=== SUMMARY ==="
if [[ $FOUND -gt 0 ]]; then
echo "[!] Found $FOUND potential issues. Review above."
exit 1
else
echo "[✓] No obvious Next.js security issues detected"
exit 0
fi
Related skills
FAQ
What does security-nextjs check in a Next.js repo?
security-nextjs checks authentication configuration, security headers, SSR and server-component data leaks, environment secret exposure, and unintended public routes. It targets Next.js conventions like middleware, API routes, and server actions before launch.
When should teams run security-nextjs?
Teams should run security-nextjs when a Next.js app is feature-complete and headed to production. It is a pre-launch hardening pass, not a substitute for runtime penetration testing or infrastructure audits.