logo
Stateful Pitfalls in Next.js: Building Secure, Stateless API Requests with Axios
by John Oba - Afrodev8 November, 2024 • 6 min read
Stateful Pitfalls in Next.js: Building Secure, Stateless API Requests with Axios

Introduction

Next.js is a popular framework for building fast, scalable web applications. Its server-side rendering (SSR) capabilities, along with serverless deployment compatibility, make it ideal for modern, stateless applications. However, managing state in Next.js can be tricky, especially when developers inadvertently introduce statefulness through global configurations. One common example of this is configuring global settings in libraries like Axios for authentication. Without careful handling, global configurations can lead to unintended behavior, such as users seeing each other’s data due to token mix-ups.

In this article, we’ll explore what it means for a server to be stateless versus stateful, and how global Axios configurations can create problems in a stateless framework like Next.js. We’ll also look at best practices to avoid these pitfalls and build secure, reliable applications.

Case Study: The Problem with Global Axios Configurations in Next.js

Consider this seemingly innocent authentication code:

export const setCookieAction = (
  cookies: ReadonlyRequestCookies,
  value: string
) => {
  cookies().set(encodedKey, value, {
    expires: Date.now() + oneDay,
  });

  // Set the global Bearer token
  axios.defaults.headers.common.Authorization = `Bearer ${value}`;
};

export const checkAuth = (
  client: AxiosStatic,
  cookies: () => ReadonlyRequestCookies
) => {
  if (axios.defaults.headers.common.Authorization) {
    return true;
  }
  const userToken = cookies().get(encodedKey);
  if (userToken?.value) {
    axios.defaults.headers.common.Authorization = `Bearer ${userToken.value}`;
    return true;
  }
  return false;
};

At first glance, this code appears to handle authentication correctly. However, it harbors a serious security flaw that can cause users to accidentally access each other's data.

Understanding the Vulnerability

The Problem

The issue stems from modifying global state (axios.defaults) in a server environment. Here's what happens:

  1. User A logs in and their token is stored in the global axios defaults
  2. User B logs in on the same server instance
  3. User B's token overwrites User A's token in the global state
  4. User A makes another request, but now their requests use User B's token
  5. User A unknowingly accesses User B's data

This creates a race condition where the last user to authenticate effectively "wins" the global token state, potentially exposing sensitive data to other users.

Why This Behavior Occurs

This happens because:

  1. Shared Global State: axios.defaults is shared across all requests within a server instance. When multiple users interact with the server, each request can potentially access and modify the shared state, leading to token mix-ups.
  2. Concurrency and Overwrites: In environments with concurrent users, any changes made to a global configuration are instantly reflected across all requests. This breaks the principle of request isolation.
  3. Stateless Nature of Next.js: Next.js doesn’t natively maintain session-specific states across requests. Any shared state becomes a liability in terms of security and data integrity.

To understand this vulnerability, we need to grasp the concept of stateful and stateless servers:

Stateless ServersStateful Servers
Each request is independentMaintain session information
No shared memory between requestsShare memory between requests
Easier to scale horizontallyMore complex to scale
More predictable behaviorCan lead to race conditions
Next.js is designed to be stateless by defaultRequire careful state management

The vulnerability occurs because we're accidentally introducing stateful behavior into a stateless environment through global variables.

The Impact

The consequences of this vulnerability can be severe:

  1. Privacy Breaches: Users can accidentally access other users' private information
  2. Security Violations: Potential for session hijacking
  3. Data Protection Laws: Possible GDPR violations
  4. Debugging Nightmares: Hard-to-reproduce issues in production

The Solution

Here's how to fix this vulnerability:

// Create an axios instance factory
const createAxiosInstance = (token?: string) => {
  const instance = axios.create();
  
  if (token) {
    instance.defaults.headers.common.Authorization = `Bearer ${token}`;
  }
  
  return instance;
};

// Request-scoped authentication
export const setCookieAction = (
  cookieStore: ReadonlyRequestCookies,
  value: string
) => {
  cookieStore.set(encodedKey, value, {
    expires: new Date(Date.now() + oneDay),
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict'
  });
  
  return createAxiosInstance(value);
};

export const checkAuth = async () => {
  const cookieStore = cookies();
  const userToken = cookieStore.get(encodedKey);
  
  if (!userToken?.value) {
    return { isAuthenticated: false, client: createAxiosInstance() };
  }
  
  return {
    isAuthenticated: true,
    client: createAxiosInstance(userToken.value)
  };
};

Key Improvements:

  1. Request Isolation: Each request gets its own axios instance
  2. Secure Cookies: Proper cookie security settings
  3. No Global State: Removed all global state modifications
  4. Type Safety: Better TypeScript types
  5. Clean Architecture: More maintainable and testable code

Best Practices for Server-Side State Management

1. Avoid Global State

  • Never use global variables for request-specific data
  • Create new instances for each request
  • Use proper dependency injection

2. Use Proper State Stores

// For persistent state
const redis = new Redis();
await redis.set('user-data', JSON.stringify(userData));

// For request-scoped state
const requestContext = new AsyncLocalStorage();

3. Implement Proper Authentication

  • Use secure session management
  • Validate tokens on every request
  • Implement proper CSRF protection

4. Handle Concurrent Requests

  • Design for concurrent access
  • Use database transactions when needed
  • Implement proper locking mechanisms

5. Error Handling and Logging

  • Implement comprehensive error tracking
  • Add request tracing
  • Monitor for suspicious patterns

Preventing Similar Issues

Here's a checklist to avoid similar vulnerabilities:

  • Audit code for global state usage
  • Implement request-scoped instances
  • Use secure cookie settings
  • Add security headers
  • Implement rate limiting
  • Use CSRF protection
  • Monitor for unauthorized access

Conclusion

In Next.Js misusing global configurations like axios.defaults serves as a reminder that even small architectural decisions can have significant security implications. When building server-side applications, it's crucial to understand the stateless nature of the environment and design our code accordingly.

By following proper state management practices and understanding the underlying architecture, we can build more secure and reliable applications. Remember: in server-side code, global state is almost always a red flag that deserves careful consideration.

Additional Resources

  1. Next.js Documentation: Server Components
  2. Node.js: AsyncLocalStorage
  3. Web Security: OWASP Session Management

Remember: security is not a feature - it's a fundamental requirement. Take the time to understand your application's architecture and make informed decisions about state management.


More Stories from Afrodev

2023 AfroDev