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:
- User A logs in and their token is stored in the global axios defaults
- User B logs in on the same server instance
- User B's token overwrites User A's token in the global state
- User A makes another request, but now their requests use User B's token
- 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:
- 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.
- 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.
- 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 Servers | Stateful Servers |
---|---|
Each request is independent | Maintain session information |
No shared memory between requests | Share memory between requests |
Easier to scale horizontally | More complex to scale |
More predictable behavior | Can lead to race conditions |
Next.js is designed to be stateless by default | Require 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:
- Privacy Breaches: Users can accidentally access other users' private information
- Security Violations: Potential for session hijacking
- Data Protection Laws: Possible GDPR violations
- 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:
- Request Isolation: Each request gets its own axios instance
- Secure Cookies: Proper cookie security settings
- No Global State: Removed all global state modifications
- Type Safety: Better TypeScript types
- 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
- Next.js Documentation: Server Components
- Node.js: AsyncLocalStorage
- 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.