In previous modules, we focused on implementing authentication, session management, the role and permission system, route protection middleware, and the Context Provider for authentication. Now we will shift our attention to the broader area of web application security, with particular focus on OWASP (Open Web Application Security Project) guidelines.
OWASP is a non-profit organization that works to improve software security. It regularly publishes the Top 10 list of the most serious security threats for web applications, which is a fundamental reference point for developers striving to create secure applications.
We will look at how to secure Next.js applications against the most serious threats from the OWASP Top 10 list:
Threat: Improper implementation of access control, allowing unauthorized users to access protected resources or perform unauthorized operations.
Protection in Next.js:
1// 1. Middleware to protect routes
2// middleware.ts
3import { NextResponse } from 'next/server';
4import type { NextRequest } from 'next/server';
5import { getToken } from 'next-auth/jwt';
6
7export async function middleware(request: NextRequest) {
8 const token = await getToken({ req: request });
9
10 // Check user permissions
11 if (!token) {
12 return NextResponse.redirect(new URL('/auth/login', request.url));
13 }
14
15 // Role-based access control
16 if (request.nextUrl.pathname.startsWith('/admin') && token.role !== 'admin') {
17 return NextResponse.redirect(new URL('/unauthorized', request.url));
18 }
19
20 return NextResponse.next();
21}
22
23// 2. Permission verification in Route Handlers
24// app/api/protected/resource/route.ts
25import { NextResponse } from 'next/server';
26import { getServerSession } from 'next-auth/next';
27import { authOptions } from '@/app/api/auth/[...nextauth]/route';
28
29export async function GET() {
30 const session = await getServerSession(authOptions);
31
32 if (!session) {
33 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
34 }
35
36 // Checking permissions for a specific resource
37 const hasPermission = await checkResourcePermission(session.user.id, 'resource-id');
38
39 if (!hasPermission) {
40 return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
41 }
42
43 // Continue with resource access...
44}
45
46// Function that checks permissions to a resource
47async function checkResourcePermission(userId: string, resourceId: string) {
48 // Implementing the permission check, e.g. via a database query
49 // It's worth implementing a caching mechanism for frequently checked permissions
50 return true; // Sample implementation
51}Additional best practices:
Threat: Improper or insufficient application of cryptography, leading to the disclosure of sensitive data.
Protection in Next.js:
1// 1. Secure password storage
2// lib/auth.ts
3import { hash, compare } from 'bcrypt';
4
5// Hashing password during registration
6export async function hashPassword(password: string): Promise<string> {
7 // We use 12 rounds for a good compromise between security and performance
8 return hash(password, 12);
9}
10
11// Verifying password during login
12export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
13 return compare(password, hashedPassword);
14}
15
16// 2. Encrypting sensitive data in the database
17// lib/encryption.ts
18import crypto from 'crypto';
19
20const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY as string; // Must be 32 bytes (256 bits)
21const IV_LENGTH = 16; // For AES, initialization vector is 16 bytes
22
23export function encrypt(text: string): string {
24 const iv = crypto.randomBytes(IV_LENGTH);
25 const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);
26 let encrypted = cipher.update(text, 'utf8', 'hex');
27 encrypted += cipher.final('hex');
28 return `${iv.toString('hex')}:${encrypted}`;
29}
30
31export function decrypt(text: string): string {
32 const [ivHex, encryptedHex] = text.split(':');
33 const iv = Buffer.from(ivHex, 'hex');
34 const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);
35 let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
36 decrypted += decipher.final('utf8');
37 return decrypted;
38}Additional best practices:
Threat: Injection of untrusted data into interpreters as part of a command or query, which can lead to execution of unauthorized commands or data access.
Protection in Next.js:
1// 1. Secure database queries (using an ORM)
2// app/api/users/route.ts
3import { NextResponse } from 'next/server';
4import { prisma } from '@/lib/prisma';
5
6export async function GET(request: Request) {
7 const { searchParams } = new URL(request.url);
8 const name = searchParams.get('name');
9
10 // Secure query using an ORM (Prisma)
11 // Instead of direct injection of parameters into an SQL query
12 const users = await prisma.user.findMany({
13 where: {
14 name: {
15 contains: name || '',
16 },
17 },
18 select: {
19 id: true,
20 name: true,
21 email: true,
22 },
23 });
24
25 return NextResponse.json(users);
26}
27
28// 2. Input data validation using the zod library
29// app/api/products/route.ts
30import { NextResponse } from 'next/server';
31import { z } from 'zod';
32
33// Validation schema
34const ProductSchema = z.object({
35 name: z.string().min(1).max(100),
36 price: z.number().positive(),
37 description: z.string().optional(),
38 categoryId: z.string().uuid(),
39});
40
41export async function POST(request: Request) {
42 try {
43 const body = await request.json();
44
45 // Validate input data
46 const result = ProductSchema.safeParse(body);
47
48 if (!result.success) {
49 return NextResponse.json(
50 { error: 'Invalid input', details: result.error.format() },
51 { status: 400 }
52 );
53 }
54
55 // Further processing of safe, validated data...
56
57 } catch (error) {
58 return NextResponse.json(
59 { error: 'Server error' },
60 { status: 500 }
61 );
62 }
63}Additional best practices:
Threat: Lack of appropriate security mechanisms at the application design level, leading to fundamental security vulnerabilities.
Protection in Next.js:
1// 1. Password security policy implementation
2// lib/passwordPolicy.ts
3export function isPasswordStrong(password: string): { isValid: boolean; reason?: string } {
4 // Minimum length of 8 characters
5 if (password.length < 8) {
6 return { isValid: false, reason: 'Password must be at least 8 characters' };
7 }
8
9 // Requires uppercase letters
10 if (!/[A-Z]/.test(password)) {
11 return { isValid: false, reason: 'Password must contain at least one uppercase letter' };
12 }
13
14 // Requires lowercase letters
15 if (!/[a-z]/.test(password)) {
16 return { isValid: false, reason: 'Password must contain at least one lowercase letter' };
17 }
18
19 // Requires digits
20 if (!/[0-9]/.test(password)) {
21 return { isValid: false, reason: 'Password must contain at least one digit' };
22 }
23
24 // Requires special characters
25 if (!/[^A-Za-z0-9]/.test(password)) {
26 return { isValid: false, reason: 'Password must contain at least one special character' };
27 }
28
29 // Check if the password is not on the list of common, easily guessable passwords
30 const commonPasswords = ['Password123!', 'Admin123!', '12345678Aa!', 'Qwerty123!']; // In practice, this should be a larger database
31 if (commonPasswords.includes(password)) {
32 return { isValid: false, reason: 'Password is too common and easy to guess' };
33 }
34
35 return { isValid: true };
36}
37
38// 2. Implementing a permission system based on the data model
39// lib/permissions.ts
40import { prisma } from '@/lib/prisma';
41
42// Model allowing certain operations only to the resource owner
43export async function canModifyResource(userId: string, resourceId: string): Promise<boolean> {
44 const resource = await prisma.resource.findUnique({
45 where: { id: resourceId },
46 select: { userId: true },
47 });
48
49 // Resource does not exist or the user is not the owner
50 if (!resource || resource.userId !== userId) {
51 return false;
52 }
53
54 return true;
55}Additional best practices:
Threat: Improper security configuration in any application component, which can lead to unauthorized access or data leakage.
Protection in Next.js:
1// 1. Content Security Policy configuration
2// next.config.js
3const ContentSecurityPolicy = `
4 default-src 'self';
5 script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.example.com;
6 style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
7 font-src 'self' https://fonts.gstatic.com;
8 img-src 'self' data: https:;
9 connect-src 'self' https://api.example.com;
10 frame-src 'self';
11`;
12
13module.exports = {
14 async headers() {
15 return [
16 {
17 source: '/(.*)',
18 headers: [
19 {
20 key: 'Content-Security-Policy',
21 value: ContentSecurityPolicy.replace(/s{2,}/g, ' ').trim(),
22 },
23 {
24 key: 'X-Content-Type-Options',
25 value: 'nosniff',
26 },
27 {
28 key: 'X-Frame-Options',
29 value: 'DENY',
30 },
31 {
32 key: 'X-XSS-Protection',
33 value: '1; mode=block',
34 },
35 {
36 key: 'Referrer-Policy',
37 value: 'strict-origin-when-cross-origin',
38 },
39 {
40 key: 'Permissions-Policy',
41 value: 'camera=(), microphone=(), geolocation=(self)',
42 },
43 ],
44 },
45 ];
46 },
47};
48
49// 2. Secure use of environment variables
50// lib/config.ts
51interface Config {
52 database: {
53 url: string;
54 };
55 auth: {
56 secret: string;
57 tokenExpiration: number;
58 };
59 services: {
60 apiKey: string;
61 endpoint: string;
62 };
63}
64
65export const config: Config = {
66 database: {
67 url: process.env.DATABASE_URL || '',
68 },
69 auth: {
70 secret: process.env.AUTH_SECRET || '',
71 tokenExpiration: Number(process.env.TOKEN_EXPIRATION) || 3600,
72 },
73 services: {
74 apiKey: process.env.SERVICE_API_KEY || '',
75 endpoint: process.env.SERVICE_ENDPOINT || '',
76 },
77};
78
79// Function to verify the configuration on application startup
80export function validateConfig() {
81 const requiredEnvVars = [
82 'DATABASE_URL',
83 'AUTH_SECRET',
84 'SERVICE_API_KEY',
85 'SERVICE_ENDPOINT',
86 ];
87
88 const missingEnvVars = requiredEnvVars.filter(
89 (envVar) => !process.env[envVar]
90 );
91
92 if (missingEnvVars.length > 0) {
93 throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);
94 }
95
96 if (process.env.AUTH_SECRET && process.env.AUTH_SECRET.length < 32) {
97 throw new Error('AUTH_SECRET should be at least 32 characters long for security');
98 }
99
100 console.log('Configuration validated successfully');
101}Additional best practices:
Threat: Using components and libraries containing known security vulnerabilities or that are no longer supported.
Protection in Next.js:
1// 1. Automatic dependency scanning
2// package.json
3{
4 "scripts": {
5 "audit": "npm audit",
6 "audit:fix": "npm audit fix",
7 "outdated": "npm outdated",
8 "update": "npm update",
9 "deps:check": "npx depcheck",
10 "prepare": "husky install"
11 },
12 "husky": {
13 "hooks": {
14 "pre-commit": "npm run audit"
15 }
16 }
17}
18
19// 2. Introducing an SRI (Subresource Integrity) policy
20// For critical external JavaScript libraries
21// app/layout.tsx (example)
22import Script from 'next/script';
23
24export default function RootLayout({ children }: { children: React.ReactNode }) {
25 return (
26 <html lang="en">
27 <head>
28 {/* Using SRI to verify the integrity of an external script */}
29 <Script
30 src="https://cdn.example.com/library.min.js"
31 integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
32 crossOrigin="anonymous"
33 />
34 </head>
35 <body>{children}</body>
36 </html>
37 );
38}Additional best practices:
npm audit or using tools like Snyk, DependabotThreat: Improper implementation of authentication mechanisms, allowing attackers to access user accounts.
Protection in Next.js:
1// 1. Login attempt limiting implementation
2// lib/rateLimit.ts
3import { Redis } from '@upstash/redis';
4
5const redis = new Redis({
6 url: process.env.UPSTASH_REDIS_URL!,
7 token: process.env.UPSTASH_REDIS_TOKEN!,
8});
9
10export async function rateLimit(
11 identifier: string, // IP or user identifier
12 limit: number, // Maximum number of attempts
13 window: number // Time window in seconds
14): Promise<{ success: boolean; remaining: number; reset: number }> {
15 const now = Math.floor(Date.now() / 1000);
16 const key = `ratelimit:${identifier}`;
17
18 // Fetch the current rate-limit data
19 const entry = await redis.hgetall(key);
20
21 if (!entry.count) {
22 // First entry
23 await redis.hset(key, {
24 count: 1,
25 reset: now + window,
26 });
27 await redis.expire(key, window);
28
29 return {
30 success: true,
31 remaining: limit - 1,
32 reset: now + window,
33 };
34 }
35
36 // Check if the time window has passed
37 if (parseInt(entry.reset) < now) {
38 // Reset the counter
39 await redis.hset(key, {
40 count: 1,
41 reset: now + window,
42 });
43 await redis.expire(key, window);
44
45 return {
46 success: true,
47 remaining: limit - 1,
48 reset: now + window,
49 };
50 }
51
52 // Check if the limit has been exceeded
53 const count = parseInt(entry.count);
54 if (count >= limit) {
55 return {
56 success: false,
57 remaining: 0,
58 reset: parseInt(entry.reset),
59 };
60 }
61
62 // Update the counter
63 await redis.hincrby(key, 'count', 1);
64
65 return {
66 success: true,
67 remaining: limit - count - 1,
68 reset: parseInt(entry.reset),
69 };
70}
71
72// 2. Login page implementation with attempt limiting
73// app/api/auth/login/route.ts
74import { NextResponse } from 'next/server';
75import { rateLimit } from '@/lib/rateLimit';
76
77export async function POST(request: Request) {
78 try {
79 const { email, password } = await request.json();
80
81 // Get the user IP address
82 const ip = request.headers.get('x-forwarded-for') || 'unknown';
83
84 // Limit login attempts: 5 attempts within 15 minutes
85 const rateLimitResult = await rateLimit(ip, 5, 15 * 60);
86
87 if (!rateLimitResult.success) {
88 return NextResponse.json(
89 {
90 error: 'Too many login attempts. Please try again later.',
91 reset: rateLimitResult.reset,
92 },
93 { status: 429 }
94 );
95 }
96
97 // Continue the login process...
98
99 } catch (error) {
100 console.error('Login error:', error);
101 return NextResponse.json(
102 { error: 'An error occurred during login' },
103 { status: 500 }
104 );
105 }
106}Additional best practices:
Threat: Lack of software and data integrity verification, allowing the introduction of incorrect or malicious components.
Protection in Next.js:
1// 1. Implementation of a digital signature mechanism for critical data
2// lib/signatures.ts
3import crypto from 'crypto';
4
5const SIGNATURE_KEY = process.env.SIGNATURE_KEY!;
6
7// Generating the signature for the data
8export function signData(data: any): string {
9 const dataString = typeof data === 'string' ? data : JSON.stringify(data);
10 return crypto
11 .createHmac('sha256', SIGNATURE_KEY)
12 .update(dataString)
13 .digest('hex');
14}
15
16// Verifying the data signature
17export function verifySignature(data: any, signature: string): boolean {
18 const expectedSignature = signData(data);
19 return crypto.timingSafeEqual(
20 Buffer.from(expectedSignature, 'hex'),
21 Buffer.from(signature, 'hex')
22 );
23}
24
25// 2. Implementation of webhook verification for external services
26// app/api/webhooks/payment-processor/route.ts
27import { NextResponse } from 'next/server';
28import { verifySignature } from '@/lib/signatures';
29
30export async function POST(request: Request) {
31 const body = await request.text();
32 const signature = request.headers.get('X-Webhook-Signature') || '';
33
34 // Verifying the webhook signature
35 if (!verifySignature(body, signature)) {
36 console.error('Invalid webhook signature');
37 return NextResponse.json(
38 { error: 'Invalid signature' },
39 { status: 401 }
40 );
41 }
42
43 // Processing the verified webhook...
44 const data = JSON.parse(body);
45
46 //
47 return NextResponse.json({ status: 'success' });
48}Additional best practices:
Threat: Insufficient logging, monitoring, and alerting of security events, which prevents detection, escalation, and response to incidents.
Protection in Next.js:
1// 1. Implementing extensive logging
2// lib/logger.ts
3import winston from 'winston';
4
5const logger = winston.createLogger({
6 level: process.env.LOG_LEVEL || 'info',
7 format: winston.format.combine(
8 winston.format.timestamp(),
9 winston.format.json()
10 ),
11 defaultMeta: { service: 'next-app' },
12 transports: [
13 // Local log saving at error level
14 new winston.transports.File({ filename: 'error.log', level: 'error' }),
15 // Local saving of all logs
16 new winston.transports.File({ filename: 'combined.log' }),
17 ],
18});
19
20// In production, send logs to a monitoring service
21if (process.env.NODE_ENV === 'production') {
22 // Example for Datadog
23 // Requires installing '@datadog/winston'
24 // import { datadog } from '@datadog/winston';
25 // logger.add(new datadog({
26 // apiKey: process.env.DATADOG_API_KEY,
27 // hostname: process.env.HOSTNAME,
28 // service: 'next-app',
29 // ddsource: 'nodejs',
30 // }));
31}
32
33// Functions for logging security events
34export function logSecurityEvent(
35 eventType: string,
36 data: any,
37 severity: 'info' | 'warn' | 'error' = 'info'
38) {
39 logger.log({
40 level: severity,
41 message: `Security event: ${eventType}`,
42 eventType,
43 data,
44 timestamp: new Date().toISOString(),
45 });
46}
47
48export function logAuthEvent(
49 userId: string | null,
50 action: 'login' | 'logout' | 'failed_login' | 'password_reset' | 'account_locked',
51 details: any = {}
52) {
53 logSecurityEvent(
54 `auth_${action}`,
55 {
56 userId,
57 ip: details.ip,
58 userAgent: details.userAgent,
59 ...details,
60 },
61 action === 'failed_login' || action === 'account_locked' ? 'warn' : 'info'
62 );
63}
64
65// 2. Implementing middleware for logging HTTP requests
66// middleware.ts
67import { NextResponse } from 'next/server';
68import type { NextRequest } from 'next/server';
69import { logSecurityEvent } from '@/lib/logger';
70
71export function middleware(request: NextRequest) {
72 // Save basic request information
73 const requestInfo = {
74 method: request.method,
75 path: request.nextUrl.pathname,
76 ip: request.ip || 'unknown',
77 userAgent: request.headers.get('user-agent') || 'unknown',
78 referer: request.headers.get('referer') || 'unknown',
79 };
80
81 // Log only relevant paths (skip static assets, etc.)
82 if (!request.nextUrl.pathname.match(/.(ico|png|jpg|jpeg|svg|css|js)$/)) {
83 logSecurityEvent('http_request', requestInfo);
84 }
85
86 // Potential attack detection
87 if (detectPotentialAttack(request)) {
88 logSecurityEvent('potential_attack', requestInfo, 'warn');
89 }
90
91 return NextResponse.next();
92}
93
94// Function that detects potential attacks based on request patterns
95function detectPotentialAttack(request: NextRequest): boolean {
96 const url = request.nextUrl.toString().toLowerCase();
97 const body = request.body; // May be unavailable in middleware
98
99 // Detection of basic attack attempts
100 const attackPatterns = [
101 /(../|..\\/)/i, // Path traversal
102 /('|")(;|--|+|%|#|\)/i, // Basic SQL injection
103 /<script>[^<]*</script>/i, // Basic XSS
104 /etc/passwd/i, // Path disclosure
105 //(wp-admin|wp-content|wp-includes)/i, // WordPress scanning
106 ];
107
108 return attackPatterns.some(pattern => pattern.test(url));
109}Additional best practices:
Threat: SSRF attacks allow an attacker to invoke HTTP requests from the application server to internal or external resources to which the server has access.
Protection in Next.js:
1// 1. Secure URL fetching implementation
2// lib/safeFetch.ts
3import { URL } from 'url';
4import fetch from 'node-fetch';
5
6// List of allowed domains
7const ALLOWED_HOSTS = [
8 'api.example.com',
9 'api.trusted-service.com',
10 'cdn.example.com',
11];
12
13// List of forbidden IP addresses (private IP ranges)
14const BLOCKED_IP_RANGES = [
15 // Local
16 /^127.d+.d+.d+$/, // 127.0.0.0/8
17 /^10.d+.d+.d+$/, // 10.0.0.0/8
18 /^172.(1[6-9]|2d|3[0-1]).d+.d+$/, // 172.16.0.0/12
19 /^192.168.d+.d+$/, // 192.168.0.0/16
20 // Link-local
21 /^169.254.d+.d+$/, // 169.254.0.0/16
22 // Multicast
23 /^2(2[4-9]|3d).d+.d+.d+$/, // 224.0.0.0/4
24];
25
26export async function safeFetch(urlString: string, options: RequestInit = {}) {
27 try {
28 // Parse URL to check its components
29 const url = new URL(urlString);
30
31 // Check if the domain is on the allowed list
32 if (!ALLOWED_HOSTS.includes(url.hostname)) {
33 throw new Error(`Host not allowed: ${url.hostname}`);
34 }
35
36 // Check if the IP address is not on the blocked list
37 // This requires additional logic for DNS resolution
38 // Simplified version of checking directly by host
39 if (BLOCKED_IP_RANGES.some(pattern => pattern.test(url.hostname))) {
40 throw new Error(`IP address not allowed: ${url.hostname}`);
41 }
42
43 // Check if the protocol is secure
44 if (url.protocol !== 'https:') {
45 throw new Error(`Protocol not allowed: ${url.protocol}`);
46 }
47
48 // Perform the HTTP request after URL verification
49 return fetch(url.toString(), options);
50 } catch (error) {
51 console.error('Safe fetch error:', error);
52 throw new Error(`Invalid or disallowed URL: ${urlString}`);
53 }
54}
55
56// 2. API proxy implementation with URL validation
57// app/api/proxy/route.ts
58import { NextResponse } from 'next/server';
59import { safeFetch } from '@/lib/safeFetch';
60
61export async function GET(request: Request) {
62 try {
63 const { searchParams } = new URL(request.url);
64 const targetUrl = searchParams.get('url');
65
66 if (!targetUrl) {
67 return NextResponse.json(
68 { error: 'URL parameter is required' },
69 { status: 400 }
70 );
71 }
72
73 try {
74 // Use the secure fetch function
75 const response = await safeFetch(targetUrl);
76 const data = await response.text();
77
78 return new NextResponse(data, {
79 status: response.status,
80 headers: {
81 'Content-Type': response.headers.get('Content-Type') || 'text/plain',
82 },
83 });
84 } catch (error) {
85 return NextResponse.json(
86 { error: 'Invalid or disallowed URL' },
87 { status: 400 }
88 );
89 }
90 } catch (error) {
91 console.error('Proxy error:', error);
92 return NextResponse.json(
93 { error: 'Server error' },
94 { status: 500 }
95 );
96 }
97}Additional best practices:
Security headers are a key element in protecting web applications. Next.js allows easy configuration of these headers in the file
next.config.js:1// next.config.js
2const securityHeaders = [
3 {
4 key: 'X-DNS-Prefetch-Control',
5 value: 'on',
6 },
7 {
8 key: 'Strict-Transport-Security',
9 value: 'max-age=63072000; includeSubDomains; preload',
10 },
11 {
12 key: 'X-XSS-Protection',
13 value: '1; mode=block',
14 },
15 {
16 key: 'X-Frame-Options',
17 value: 'SAMEORIGIN',
18 },
19 {
20 key: 'X-Content-Type-Options',
21 value: 'nosniff',
22 },
23 {
24 key: 'Referrer-Policy',
25 value: 'strict-origin-when-cross-origin',
26 },
27 {
28 key: 'Permissions-Policy',
29 value: 'camera=(), microphone=(), geolocation=(self), interest-cohort=()',
30 },
31];
32
33module.exports = {
34 async headers() {
35 return [
36 {
37 // Apply these headers to all paths
38 source: '/(.*)',
39 headers: securityHeaders,
40 },
41 ];
42 },
43 // Other Next.js configurations...
44};Tests are a key element in ensuring application security. Below is an example of integrating security tests with the CI/CD process:
1# .github/workflows/security.yml
2name: Security Checks
3
4on:
5 push:
6 branches: [ main, develop ]
7 pull_request:
8 branches: [ main, develop ]
9 schedule:
10 - cron: '0 0 * * 0' # Weekly scan
11
12jobs:
13 security-checks:
14 runs-on: ubuntu-latest
15 steps:
16 - uses: actions/checkout@v2
17
18 - name: Setup Node.js
19 uses: actions/setup-node@v2
20 with:
21 node-version: '16'
22
23 - name: Install dependencies
24 run: npm ci
25
26 - name: Run npm audit
27 run: npm audit --audit-level=high
28
29 - name: OWASP ZAP Scan
30 uses: zaproxy/action-baseline@v0.6.1
31 with:
32 target: 'https://staging.example.com' # Your staging environment URL
33
34 - name: Run Snyk to check for vulnerabilities
35 uses: snyk/actions/node@master
36 env:
37 SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
38
39 - name: Check for secrets in code
40 uses: gitleaks/gitleaks-action@v1.6.0Below is a checklist you can use to assess the security of your Next.js application:
Web application security, including Next.js applications, requires a comprehensive approach and constant attention. In this module, we covered the most important threats defined in the OWASP Top 10 and how to minimize them in the context of Next.js applications.
Key points to remember:
Remember that security is a process, not a product. It requires constant improvement and adaptation to new threats.
In the next module, we will look at security auditing and monitoring to better understand how to detect and respond to potential security incidents.