0.0",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.3.0",
"snyk": "^1.130.0"
}
}
// File: middleware.ts
// Vulnerable middleware: no rate limiting, unvalidated redirect
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// VULNERABLE: Unvalidated redirect from query parameter
const redirectUrl = request.nextUrl.searchParams.get('redirect');
if (redirectUrl) {
return NextResponse.redirect(new URL(redirectUrl, request.url));
}
// VULNERABLE: No CSRF protection on server actions
const response = NextResponse.next();
response.headers.set('x-powered-by', 'Next.js'); // VULNERABLE: Information disclosure
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
// File: app/api/users/route.ts
// Vulnerable API route: SQL injection, no auth, plain text password storage
import { NextRequest, NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
// Mock user database (insecure, no parameterized queries)
const users: Array<{ id: string; email: string; password: string }> = [];
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json();
// VULNERABLE: No input validation for email/password
// VULNERABLE: SQL injection if using real DB (simulated here)
const existingUser = users.find(u => u.email === email);
if (existingUser) {
return NextResponse.json({ error: 'User exists' }, { status: 400 });
}
// VULNERABLE: Weak password hashing (low rounds)
const hashedPassword = await bcrypt.hash(password, 4); // Should be 12+ rounds
// VULNERABLE: Hardcoded JWT secret
const token = jwt.sign({ email }, 'hardcoded-secret-123', { expiresIn: '1h' });
users.push({ id: crypto.randomUUID(), email, password: hashedPassword });
return NextResponse.json({ token }, { status: 201 });
} catch (error) {
console.error('User creation failed:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// File: lib/actions.ts
// Vulnerable server action: no auth, XSS, unvalidated input
'use server';
import { revalidatePath } from 'next/cache';
export async function submitComment(formData: FormData) {
const comment = formData.get('comment') as string;
// VULNERABLE: No input sanitization, XSS possible
// VULNERABLE: No authentication check
console.log(New comment: ${comment}); // Simulated storage
revalidatePath('/comments');
return { success: true, comment };
}
// File: .snyk
// Snyk 1.130 configuration for Next.js 15 projects
// Ignore low-severity vulnerabilities in dev dependencies, set custom rules
version: v1.25.0
ignore: {}
Custom vulnerability rules for Next.js 15 specific patterns
rules:
- id: SNYK-JS-NEXT-1000000 # Hypothetical Next.js 15 middleware bypass (example)
comment: "Next.js 15 middleware auth bypass in canary versions, patched in 15.0.1"
expires: 2025-01-01
paths:
- id: SNYK-JS-JSONWEBTOKEN-1000001 # Hardcoded JWT secret
comment: "Hardcoded JWT secret in API routes, use env vars"
severity: high
paths:
// File: .github/workflows/snyk-scan.yml
// GitHub Actions workflow for Snyk 1.130 SAST scanning on every PR
name: Snyk SAST Scan
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
snyk-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Snyk 1.130
run: npm install -g snyk@1.130.0
- name: Authenticate Snyk
run: snyk auth ${{ secrets.SNYK_TOKEN }}
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Run Snyk test
run: |
snyk test --all-projects --json > snyk-results.json
snyk monitor --all-projects --org=your-snyk-org
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Upload Snyk results
uses: actions/upload-artifact@v4
with:
name: snyk-results
path: snyk-results.json
- name: Fail on critical vulnerabilities
run: |
CRITICAL_COUNT=$(jq '.vulnerabilities | map(select(.severity == "critical")) | length' snyk-results.json)
if [ $CRITICAL_COUNT -gt 0 ]; then
echo "Found $CRITICAL_COUNT critical vulnerabilities, failing build"
exit 1
fi
// File: scripts/snyk-report.ts
// Generate human-readable Snyk report from JSON output
import fs from 'fs';
import jq from 'jq-node';
interface SnykVulnerability {
id: string;
severity: 'low' | 'medium' | 'high' | 'critical';
title: string;
packageName: string;
version: string;
patchedIn: string | null;
}
async function generateSnykReport() {
try {
const rawResults = fs.readFileSync('./snyk-results.json', 'utf-8');
const results = JSON.parse(rawResults);
if (!results.vulnerabilities) {
console.log('No vulnerabilities found!');
return;
}
const vulnerabilities: SnykVulnerability[] = results.vulnerabilities;
const groupedBySeverity = vulnerabilities.reduce((acc, vuln) => {
acc[vuln.severity] = acc[vuln.severity] || [];
acc[vuln.severity].push(vuln);
return acc;
}, {} as Record);
console.log('=== Snyk 1.130 Scan Report ===');
console.log(`Total vulnerabilities: ${vulnerabilities.length}`);
console.log(`Critical: ${groupedBySeverity.critical?.length || 0}`);
console.log(`High: ${groupedBySeverity.high?.length || 0}`);
console.log(`Medium: ${groupedBySeverity.medium?.length || 0}`);
console.log(`Low: ${groupedBySeverity.low?.length || 0}`);
console.log('\n--- Critical Vulnerabilities ---');
groupedBySeverity.critical?.forEach(vuln => {
console.log(`- ${vuln.title} (${vuln.id})`);
console.log(` Package: ${vuln.packageName}@${vuln.version}`);
console.log(` Patched in: ${vuln.patchedIn || 'No patch available'}`);
});
} catch (error) {
console.error('Failed to generate Snyk report:', error);
process.exit(1);
}
}
generateSnykReport();
## Pitfall Guide
1. **Hardcoded Secrets in Server Actions/API Routes:** Embedding JWT secrets or API keys directly in source files triggers CWE-798. Always inject secrets via environment variables and validate their presence at build/runtime.
2. **Weak Cryptographic Parameters:** Using `bcrypt` with rounds < 12 (e.g., `4`) drastically reduces hash computation time, enabling brute-force attacks. Enforce minimum rounds via linting rules or custom Snyk policies.
3. **Unvalidated Redirects in Middleware:** Extracting URLs directly from `request.nextUrl.searchParams` without allowlist validation creates open redirects (CWE-601). Implement strict domain whitelisting before calling `NextResponse.redirect()`.
4. **Missing Authentication on Server Actions:** `'use server'` functions bypass client-side guards. Without explicit session/token validation, they expose data mutation endpoints to unauthenticated actors (CWE-285).
5. **Information Disclosure via Headers:** Setting `x-powered-by` or exposing framework versions in responses aids reconnaissance. Strip these headers in middleware or reverse proxy configurations.
6. **Relying Solely on SAST for Runtime Context:** Static analysis cannot detect reflected XSS or CSRF flaws triggered by HTTP request flows. Pair Snyk with OWASP ZAP 2.13 DAST scans to cover client-server interaction vectors.
## Deliverables
- **π¦ Vulnerable Sample Blueprint:** Complete Next.js 15 App Router project demonstrating 7 intentional OWASP Top 10 vulnerabilities for safe testing and scanner calibration.
- **β
Security Hardening Checklist:** Mapped remediation steps for each detected CWE, including middleware hardening, server action auth guards, and cryptographic parameter enforcement.
- **βοΈ Configuration Templates:** Production-ready `.snyk` custom rule definitions, GitHub Actions CI/CD workflow for automated SAST/DAST enforcement, and TypeScript reporting scripts for JSON-to-human-readable vulnerability triage.