We use cookies to enhance your experience on the site
CodeWorlds

Application security and OWASP

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.

OWASP Top 10 and its application in Next.js apps

We will look at how to secure Next.js applications against the most serious threats from the OWASP Top 10 list:

1. Broken Access Control

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:

  1. Implement access control at the server level, never rely solely on client-side security
  2. Apply the default deny principle (deny by default)
  3. Regularly review and test access control mechanisms
  4. Implement an account lockout mechanism after multiple failed login attempts

2. Cryptographic Failures

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:

  1. Always use HTTPS for all application pages
  2. Implement appropriate security-related HTTP headers (HSTS, CSP)
  3. Regularly update cryptographic libraries
  4. Implement a cryptographic key rotation strategy
  5. Do not store sensitive data in local browser storages (localStorage, sessionStorage)

3. Injection

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:

  1. Filter and sanitize input and output data
  2. Use complex, randomized passwords and session identifiers
  3. Implement session timeout and limit login attempts
  4. Escape all data before placing it in SQL commands, scripts, or HTML
  5. Avoid dynamic generation of JavaScript code by string concatenation

4. Insecure Design

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:

  1. Implement a multi-layered protection mechanism (defense in depth)
  2. Perform threat modeling at an early stage of application development
  3. Design with the principle of least privilege in mind
  4. Treat user data as untrusted and always validate it
  5. Regularly review and update security mechanisms

5. Security Misconfiguration

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:

  1. Remove or disable unnecessary features, components, and dependencies
  2. Regularly update all application components and dependencies
  3. Implement a configuration auditing mechanism
  4. Never place sensitive data in source code or publicly accessible files
  5. Use different configurations for development and production environments

6. Vulnerable and Outdated Components

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:

  1. Regularly running
    npm audit
    or using tools like Snyk, Dependabot
  2. Removing unused dependencies
  3. Subscribing to notifications about security vulnerabilities in used libraries
  4. Using trusted, well-maintained libraries with an active community
  5. Preparing procedures for quick component updates after vulnerability discovery

7. Identification and Authentication Failures

Threat: 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:

  1. Implement strong password requirements
  2. Require multi-factor authentication (MFA) for administrator accounts and users with high permissions
  3. Do not reveal detailed information about authentication errors
  4. Implement secure password recovery mechanisms
  5. Use one-time token mechanisms for critical operations

8. Software and Data Integrity Failures

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:

  1. Verify the integrity of files and source code in the CI/CD process
  2. Verify the origin of external libraries and components
  3. Use SRI (Subresource Integrity) mechanisms for external resources
  4. Implement automated security tests in the CI/CD process
  5. Monitor and analyze changes in configuration files and application code

9. Security Logging and Monitoring Failures

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:

  1. Implement a centralized logging and monitoring system
  2. Configure automatic alerting for suspicious events
  3. Provide mechanisms for correlating events from different parts of the application
  4. Regularly analyze logs for unauthorized activities
  5. Store logs for an appropriately long time in accordance with regulations and business needs

10. Server-Side Request Forgery

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:

  1. Implement allow-lists of domains instead of block-lists
  2. Use a network firewall to block connections to internal resources
  3. Filter and sanitize input data containing URLs
  4. Use minimal permissions for service and application accounts
  5. Avoid passing full responses from external APIs to the client

Implementing Security Headers in Next.js

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};

Penetration testing and security testing automation

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.0

Security checklist for Next.js applications

Below is a checklist you can use to assess the security of your Next.js application:

General security

  • [ ] Appropriate security-related HTTP headers have been implemented
  • [ ] HTTPS is used for all environments
  • [ ] Input data validation mechanism has been implemented
  • [ ] Inactive sessions are automatically logged out
  • [ ] Sensitive information is not stored in publicly accessible files
  • [ ] Logs do not contain sensitive data

Authentication and authorization

  • [ ] Passwords are properly hashed and salted
  • [ ] Login attempt limiting mechanism has been implemented
  • [ ] Password security policy has been implemented
  • [ ] Role and permission system works correctly
  • [ ] Sessions are securely managed
  • [ ] Multi-factor authentication (MFA) has been implemented for administrator accounts

Data processing

  • [ ] Data is properly sanitized before rendering
  • [ ] Database queries are parameterized (protected against SQL Injection)
  • [ ] XSS prevention mechanisms have been implemented
  • [ ] API endpoints are protected against unauthorized access
  • [ ] CSRF token implementation prevents CSRF attacks

Dependencies and code

  • [ ] All dependencies are up to date and free of known vulnerabilities
  • [ ] Code is regularly scanned for security vulnerabilities
  • [ ] Best coding practices are applied
  • [ ] There are no hardcoded secrets and keys in the code

Monitoring and incident response

  • [ ] Security event logging mechanism has been implemented
  • [ ] Security incident response procedures have been defined
  • [ ] Regular penetration tests are conducted
  • [ ] Monitoring and alerting mechanisms have been implemented

Summary

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:

  1. Comprehensive approach - security should be considered at every stage of application development
  2. Principle of least permissions - grant minimum permissions necessary to perform the task
  3. Validation and sanitization - always validate and sanitize input data
  4. Updates and patches - regularly update dependencies and apply security patches
  5. Monitoring and logging - implement extensive monitoring and logging mechanisms
  6. Testing - regularly perform security tests

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.

Go to CodeWorlds