Agent Skill
2/7/2026cyber-security-core-skills
Core web application security patterns for Next.js 16. Use when implementing input validation, SQL injection prevention, XSS protection, CSRF defense, and secure coding practices. Essential for building cyber-hardened applications.
L
lewisperez999
0GitHub Stars
2Views
npx skills add lewisperez999/digital-twin-iii
SKILL.md
| Name | cyber-security-core-skills |
| Description | Core web application security patterns for Next.js 16. Use when implementing input validation, SQL injection prevention, XSS protection, CSRF defense, and secure coding practices. Essential for building cyber-hardened applications. |
name: cyber-security-core-skills description: Core web application security patterns for Next.js 16. Use when implementing input validation, SQL injection prevention, XSS protection, CSRF defense, and secure coding practices. Essential for building cyber-hardened applications. license: MIT
Cyber Security Core Skills
Comprehensive security patterns for building cyber-hardened Next.js 16 applications with defense-in-depth strategies.
Table of Contents
- Input Validation & Sanitization
- SQL Injection Prevention
- XSS Protection
- CSRF Protection
- Security Headers
- Secure API Design
- Error Handling
- Best Practices
Input Validation & Sanitization
Zod Schema Validation
Always validate all inputs at the API boundary:
// lib/validations/user.ts
import { z } from 'zod';
// Strict input schemas with security constraints
export const userInputSchema = z.object({
email: z
.string()
.email('Invalid email format')
.max(255, 'Email too long')
.toLowerCase()
.trim(),
username: z
.string()
.min(3, 'Username too short')
.max(30, 'Username too long')
.regex(/^[a-zA-Z0-9_-]+$/, 'Only alphanumeric, underscore, and hyphen allowed'),
password: z
.string()
.min(12, 'Password must be at least 12 characters')
.max(128, 'Password too long')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[a-z]/, 'Must contain lowercase')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special character'),
bio: z
.string()
.max(500, 'Bio too long')
.optional()
.transform(val => val ? sanitizeHtml(val) : val),
});
// Search input with injection prevention
export const searchSchema = z.object({
query: z
.string()
.max(100, 'Search query too long')
.transform(val => val.replace(/[<>\"\'%;()&+]/g, '')), // Remove dangerous chars
page: z.coerce.number().int().positive().max(1000).default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
});
Server Action Validation
// app/actions/user.ts
'use server';
import { userInputSchema } from '@/lib/validations/user';
import { revalidatePath } from 'next/cache';
export async function createUser(formData: FormData) {
// Parse and validate all input
const rawInput = {
email: formData.get('email'),
username: formData.get('username'),
password: formData.get('password'),
bio: formData.get('bio'),
};
const result = userInputSchema.safeParse(rawInput);
if (!result.success) {
// Log validation failure for security monitoring
console.warn('[SECURITY] Validation failed:', {
timestamp: new Date().toISOString(),
errors: result.error.flatten(),
// Never log actual input values
});
return {
success: false,
error: 'Invalid input provided',
fieldErrors: result.error.flatten().fieldErrors,
};
}
const validatedData = result.data;
// Proceed with validated data only
// ...
}
HTML Sanitization
// lib/security/sanitize.ts
import DOMPurify from 'isomorphic-dompurify';
export function sanitizeHtml(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: [],
KEEP_CONTENT: true,
});
}
export function sanitizeForDisplay(input: string): string {
return input
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
export function sanitizeFilename(filename: string): string {
return filename
.replace(/[^a-zA-Z0-9._-]/g, '_')
.replace(/\.{2,}/g, '.')
.substring(0, 255);
}
SQL Injection Prevention
Parameterized Queries with Prisma
Always use Prisma's parameterized queries:
// SECURE: Prisma automatically parameterizes
const user = await prisma.user.findUnique({
where: { email: userInput }, // Safe - parameterized
});
// SECURE: Using Prisma's built-in methods
const users = await prisma.user.findMany({
where: {
OR: [
{ email: { contains: searchTerm } },
{ username: { contains: searchTerm } },
],
},
});
// DANGEROUS: Never do this!
// const user = await prisma.$queryRaw`SELECT * FROM users WHERE email = '${userEmail}'`;
// SECURE: If raw queries needed, use parameterized version
const users = await prisma.$queryRaw`
SELECT * FROM users
WHERE email = ${email}
AND status = ${status}
`;
Query Builder Safety
// lib/db/safe-queries.ts
import { Prisma } from '@prisma/client';
// Safe dynamic query building
export function buildUserSearchQuery(filters: {
email?: string;
username?: string;
role?: string;
createdAfter?: Date;
}) {
const where: Prisma.UserWhereInput = {};
if (filters.email) {
where.email = { contains: filters.email, mode: 'insensitive' };
}
if (filters.username) {
where.username = { contains: filters.username, mode: 'insensitive' };
}
if (filters.role) {
// Validate against allowed values
const allowedRoles = ['user', 'admin', 'moderator'];
if (allowedRoles.includes(filters.role)) {
where.role = filters.role;
}
}
if (filters.createdAfter) {
where.createdAt = { gte: filters.createdAfter };
}
return where;
}
XSS Protection
Content Security Policy
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Content Security Policy
const csp = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Adjust for your needs
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.yourdomain.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; ');
response.headers.set('Content-Security-Policy', csp);
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-XSS-Protection', '1; mode=block');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
return response;
}
Safe Content Rendering
// components/SafeContent.tsx
'use client';
import DOMPurify from 'dompurify';
import { useMemo } from 'react';
interface SafeContentProps {
html: string;
allowedTags?: string[];
}
export function SafeContent({ html, allowedTags }: SafeContentProps) {
const sanitizedHtml = useMemo(() => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: allowedTags || ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
ADD_ATTR: ['target'], // Force target="_blank" on links
FORBID_TAGS: ['script', 'style', 'iframe', 'form', 'input'],
});
}, [html, allowedTags]);
return (
<div
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
className="prose"
/>
);
}
// Usage - never render user content without sanitization
// <SafeContent html={userGeneratedContent} />
Escape User Input in Templates
// Always use React's built-in escaping
function UserProfile({ user }: { user: User }) {
return (
<div>
{/* SAFE: React escapes by default */}
<h1>{user.name}</h1>
<p>{user.bio}</p>
{/* DANGEROUS: Only use with sanitized content */}
{/* <div dangerouslySetInnerHTML={{ __html: user.bio }} /> */}
</div>
);
}
CSRF Protection
Server Actions with Validation
// app/actions/secure-action.ts
'use server';
import { headers } from 'next/headers';
async function validateRequest() {
const headersList = await headers();
const origin = headersList.get('origin');
const host = headersList.get('host');
// Validate origin matches host
if (origin) {
const originUrl = new URL(origin);
if (originUrl.host !== host) {
throw new Error('Invalid request origin');
}
}
return true;
}
export async function secureAction(formData: FormData) {
await validateRequest();
// Proceed with action
}
CSRF Token Implementation
// lib/security/csrf.ts
import { cookies } from 'next/headers';
import { randomBytes, createHmac } from 'crypto';
const CSRF_SECRET = process.env.CSRF_SECRET!;
export async function generateCsrfToken(): Promise<string> {
const token = randomBytes(32).toString('hex');
const signature = createHmac('sha256', CSRF_SECRET)
.update(token)
.digest('hex');
const cookieStore = await cookies();
cookieStore.set('csrf_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 3600, // 1 hour
});
return `${token}.${signature}`;
}
export async function validateCsrfToken(clientToken: string): Promise<boolean> {
const cookieStore = await cookies();
const storedToken = cookieStore.get('csrf_token')?.value;
if (!storedToken || !clientToken) return false;
const [token, signature] = clientToken.split('.');
if (token !== storedToken) return false;
const expectedSignature = createHmac('sha256', CSRF_SECRET)
.update(token)
.digest('hex');
return signature === expectedSignature;
}
Security Headers
Next.js Config Headers
// next.config.ts
import type { NextConfig } from 'next';
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
];
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
];
},
};
export default nextConfig;
Secure API Design
API Route Protection
// app/api/secure/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { rateLimit } from '@/lib/security/rate-limit';
import { validateApiKey } from '@/lib/security/api-key';
const requestSchema = z.object({
action: z.enum(['read', 'write', 'delete']),
resourceId: z.string().uuid(),
});
export async function POST(request: NextRequest) {
try {
// 1. Rate limiting
const ip = request.headers.get('x-forwarded-for') || 'unknown';
const rateLimitResult = await rateLimit(ip, 'api', 100, 60000);
if (!rateLimitResult.success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'Retry-After': String(rateLimitResult.retryAfter),
'X-RateLimit-Limit': '100',
'X-RateLimit-Remaining': '0',
},
}
);
}
// 2. API Key validation
const apiKey = request.headers.get('x-api-key');
if (!apiKey || !await validateApiKey(apiKey)) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// 3. Input validation
const body = await request.json();
const result = requestSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: 'Invalid request', details: result.error.flatten() },
{ status: 400 }
);
}
// 4. Process request with validated data
const { action, resourceId } = result.data;
// ... business logic
return NextResponse.json({ success: true });
} catch (error) {
// Never expose internal errors
console.error('[API_ERROR]', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Error Handling
Secure Error Responses
// lib/security/error-handler.ts
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public isOperational: boolean = true,
public code?: string
) {
super(message);
Error.captureStackTrace(this, this.constructor);
}
}
// Safe error messages for clients
const SAFE_ERROR_MESSAGES: Record<string, string> = {
VALIDATION_ERROR: 'Invalid input provided',
AUTH_ERROR: 'Authentication failed',
NOT_FOUND: 'Resource not found',
FORBIDDEN: 'Access denied',
RATE_LIMIT: 'Too many requests',
DEFAULT: 'An error occurred',
};
export function getSafeErrorMessage(error: unknown): string {
if (error instanceof AppError && error.isOperational) {
return error.message;
}
// Never expose internal error details
return SAFE_ERROR_MESSAGES.DEFAULT;
}
export function handleApiError(error: unknown) {
// Log full error internally
console.error('[ERROR]', {
timestamp: new Date().toISOString(),
error: error instanceof Error ? {
name: error.name,
message: error.message,
stack: error.stack,
} : error,
});
// Return safe response
const statusCode = error instanceof AppError ? error.statusCode : 500;
const message = getSafeErrorMessage(error);
return { statusCode, message };
}
Best Practices
Security Checklist
-
Input Validation
- Validate ALL inputs at API boundaries
- Use strict schemas with type coercion
- Sanitize HTML content before storage and display
- Limit input lengths to prevent DoS
-
Output Encoding
- Use React's built-in escaping
- Sanitize any dangerouslySetInnerHTML usage
- Set appropriate Content-Type headers
-
Authentication
- Use secure session management
- Implement proper password hashing (bcrypt/argon2)
- Enable MFA for sensitive operations
-
Authorization
- Check permissions on every request
- Use principle of least privilege
- Validate ownership before modifications
-
Data Protection
- Encrypt sensitive data at rest
- Use HTTPS for all communications
- Implement proper key management
-
Logging & Monitoring
- Log security events
- Never log sensitive data (passwords, tokens)
- Set up alerts for suspicious activity
Dependencies to Install
npm install zod isomorphic-dompurify
npm install -D @types/dompurify
Environment Variables
# .env.local
CSRF_SECRET=your-32-character-random-secret
API_KEY_SECRET=your-api-key-encryption-secret
Troubleshooting
Common Issues
-
CSP Blocking Resources
- Check browser console for CSP violations
- Adjust CSP directives for legitimate resources
- Use nonces for inline scripts if needed
-
CORS Errors
- Configure allowed origins explicitly
- Don't use
*in production
-
Validation Bypasses
- Ensure validation runs server-side
- Never trust client-side validation alone
Skills Info
Original Name:cyber-security-core-skillsAuthor:lewisperez999
Download