Skip to main content

Server Actions Wrapper

Overview​

The Server Actions wrapper (secureServerAction) protects Next.js Server Actions from prototype pollution, injection attacks, and validates all input arguments automatically.

Why Use the Wrapper​

Server Actions are particularly vulnerable because:

  • Data comes directly from forms
  • No URL-based routing protection
  • Arguments passed directly to functions
  • Easy to forget validation

The wrapper provides:

  • Automatic argument validation
  • Input sanitization
  • CSRF protection
  • Prototype pollution prevention
  • Injection detection

Implementation​

/**
* Create a secure server action wrapper
*/
export function createSecureServerAction(config: FortressConfig) {
const logger = new FortressLogger(config.logging);
const deserializationValidator = createDeserializationValidator(
config.modules.deserialization
);
const injectionValidator = createInjectionValidator(config.modules.injection);
const csrfValidator = config.modules.csrf.enabled
? createCSRFValidator(config.modules.csrf)
: null;

return function secureServerAction<TArgs extends unknown[], TReturn>(
action: (...args: TArgs) => Promise<TReturn>,
options: SecureActionOptions = {}
) {
return async function securedAction(...args: TArgs): Promise<TReturn> {
// 1. Validate deserialization
if (config.modules.deserialization.enabled) {
for (const arg of args) {
const result = deserializationValidator.validate(arg);
if (!result.valid) {
logger.warn(`Server action blocked: ${result.message}`);
throw new SecurityError(
result.message || 'Invalid input detected',
result.rule || 'deserialization'
);
}
}
}

// 2. Validate injection
if (config.modules.injection.enabled) {
for (const arg of args) {
const result = injectionValidator.validate(arg);
if (!result.valid) {
logger.warn(
`Injection attempt in server action: ${result.message}`
);
throw new SecurityError(
result.message || 'Injection detected',
result.rule || 'injection'
);
}
}
}

// 3. CSRF validation (if required)
if (options.requireCSRF && csrfValidator) {
const csrfToken = extractCSRFToken(args);
const sessionId = extractSessionId(args);

const result = await csrfValidator.validate(
csrfToken,
sessionId,
'POST'
);
if (!result.valid) {
logger.warn(`CSRF validation failed: ${result.message}`);
throw new SecurityError(
result.message || 'CSRF validation failed',
result.rule || 'csrf'
);
}
}

// 4. Sanitize inputs if requested
let sanitizedArgs = args;
if (options.sanitizeInputs) {
sanitizedArgs = sanitizeInputs(args) as TArgs;
}

// Execute the original action with sanitized inputs
return await action(...sanitizedArgs);
};
};
}

Basic Usage​

Setup​

// fortress.config.ts
import { FortressConfig } from '@mindfiredigital/nextjs-fortress';

export const fortressConfig: FortressConfig = {
enabled: true,
mode: 'development',
// ... your config
};

// lib/fortress.ts
import { createSecureServerAction } from '@mindfiredigital/nextjs-fortress';
import { fortressConfig } from '../fortress.config';

export const secureServerAction = createSecureServerAction(fortressConfig);

Simple Protection​

// app/actions.ts
'use server';

import { secureServerAction } from '@/lib/fortress';

export const updateProfile = secureServerAction(
async (userId: string, profile: ProfileData) => {
// All arguments validated:
// ✓ No prototype pollution
// ✓ No SQL injection
// ✓ No XSS

await db.profiles.update(userId, profile);
return { success: true };
}
);

Advanced Configuration​

With Input Sanitization​

// app/actions.ts
'use server';

import { secureServerAction } from '@/lib/fortress';

export const createPost = secureServerAction(
async (title: string, content: string) => {
// Inputs sanitized:
// - __proto__ removed
// - constructor removed
// - prototype removed

const post = await db.posts.create({ title, content });
return { success: true, post };
},
{
sanitizeInputs: true, // Remove dangerous keys
}
);

With CSRF Protection​

// app/actions.ts
'use server';

import { secureServerAction } from '@/lib/fortress';

export const deleteAccount = secureServerAction(
async (userId: string, metadata: ActionMetadata) => {
// CSRF token validated from metadata._csrf
await db.users.delete(userId);
return { success: true };
},
{
requireCSRF: true, // Validate CSRF token
}
);

// Client usage:
interface ActionMetadata {
_csrf: string;
sessionId: string;
}

await deleteAccount(userId, {
_csrf: getCsrfToken(),
sessionId: getSessionId(),
});

With Input Restrictions​

// app/actions.ts
'use server';

import { secureServerAction } from '@/lib/fortress';

export const updateSettings = secureServerAction(
async (settings: Settings) => {
await db.settings.update(settings);
return { success: true };
},
{
sanitizeInputs: true,
allowedInputs: ['theme', 'language', 'notifications'], // Only these keys
}
);

With Depth Limiting​

// app/actions.ts
'use server';

import { secureServerAction } from '@/lib/fortress';

export const processComplexData = secureServerAction(
async (data: ComplexData) => {
const result = await processData(data);
return { success: true, result };
},
{
maxDepth: 5, // Limit nesting to 5 levels
sanitizeInputs: true,
}
);

Options Reference​

interface SecureActionOptions {
// Require CSRF token validation
requireCSRF?: boolean;

// Sanitize inputs (remove dangerous keys)
sanitizeInputs?: boolean;

// Maximum nesting depth
maxDepth?: number;

// Allowed input keys (whitelist)
allowedInputs?: string[];

// Rate limit key for this action
rateLimitKey?: string;
}

Real-World Examples​

Form Submission​

// app/actions.ts
'use server';

import { secureServerAction } from '@/lib/fortress';
import { revalidatePath } from 'next/cache';

export const submitContactForm = secureServerAction(
async (formData: ContactForm) => {
// Validated: no injection, no prototype pollution
await db.contacts.create(formData);
await sendEmail(formData.email, 'Thank you');

revalidatePath('/contact');
return { success: true };
},
{
sanitizeInputs: true, // Clean the input
}
);

// Component usage:
// app/contact/page.tsx
'use client';

export default function ContactPage() {
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target);

const result = await submitContactForm({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});

if (result.success) {
alert('Form submitted!');
}
};

return <form onSubmit={handleSubmit}>...</form>;
}

User Authentication​

// app/actions.ts
'use server';

import { secureServerAction } from '@/lib/fortress';

export const login = secureServerAction(
async (username: string, password: string) => {
// Validated: no SQL injection in username
const user = await authenticateUser(username, password);

if (!user) {
throw new Error('Invalid credentials');
}

await createSession(user.id);
return { success: true, user };
},
{
sanitizeInputs: true,
}
);

Data Update​

// app/actions.ts
'use server';

import { secureServerAction } from '@/lib/fortress';

export const updateUser = secureServerAction(
async (userId: string, updates: UserUpdates) => {
// Dangerous keys removed automatically
// No __proto__, constructor, prototype

await db.users.update(userId, updates);
return { success: true };
},
{
sanitizeInputs: true,
allowedInputs: ['name', 'email', 'bio'], // Only these fields
}
);

File Upload Processing​

// app/actions.ts
'use server';

import { secureServerAction } from '@/lib/fortress';

export const processUpload = secureServerAction(
async (fileMetadata: FileMetadata) => {
// Metadata validated for injection
const result = await processFile(fileMetadata);
return { success: true, result };
},
{
sanitizeInputs: true,
maxDepth: 3, // Shallow metadata only
}
);

Admin Operations​

// app/admin/actions.ts
'use server';

import { secureServerAction } from '@/lib/fortress';

export const promoteUser = secureServerAction(
async (userId: string, role: string, metadata: ActionMetadata) => {
// CSRF validated
// Inputs sanitized

await db.users.update(userId, { role });
return { success: true };
},
{
requireCSRF: true,
sanitizeInputs: true,
}
);

Input Sanitization​

The wrapper automatically removes dangerous keys:

// Input:
{
name: "John",
email: "john@example.com",
__proto__: { isAdmin: true },
constructor: { prototype: { hacked: true } }
}

// After sanitization:
{
name: "John",
email: "john@example.com"
}
// __proto__ and constructor removed!

Nested Sanitization​

// Input:
{
user: {
profile: {
name: "John",
__proto__: { admin: true }
}
}
}

// After sanitization:
{
user: {
profile: {
name: "John"
}
}
}
// Dangerous keys removed at all levels

Error Handling​

Validation Error​

// Client code:
try {
await updateProfile(userId, {
__proto__: { isAdmin: true }
});
} catch (error) {
if (error instanceof SecurityError) {
console.error('Security validation failed:', error.message);
// Error: "Dangerous key detected: __proto__"
}
}

Injection Detected​

try {
await createPost(
"Title",
"Content with <script>alert(1)</script>"
);
} catch (error) {
// Error: "XSS injection detected: <script>"
}

CSRF Validation Failed​

try {
await deleteAccount(userId, {
_csrf: 'invalid-token'
});
} catch (error) {
// Error: "CSRF validation failed"
}

Client-Side Usage​

With Form​

// app/profile/page.tsx
'use client';

import { updateProfile } from '@/app/actions';

export default function ProfilePage() {
const handleSubmit = async (formData: FormData) => {
try {
const result = await updateProfile(
userId,
{
name: formData.get('name'),
bio: formData.get('bio'),
}
);

if (result.success) {
toast.success('Profile updated!');
}
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
}
}
};

return <form action={handleSubmit}>...</form>;
}

With State Management​

// app/posts/page.tsx
'use client';

import { useState } from 'react';
import { createPost } from '@/app/actions';

export default function CreatePostPage() {
const [loading, setLoading] = useState(false);

const handleCreate = async (title: string, content: string) => {
setLoading(true);

try {
const result = await createPost(title, content);

if (result.success) {
router.push('/posts');
}
} catch (error) {
console.error('Failed to create post:', error);
} finally {
setLoading(false);
}
};

return <PostForm onSubmit={handleCreate} />;
}

Summary​

secureServerAction wrapper provides:

  • Automatic argument validation
  • Input sanitization
  • CSRF protection
  • Injection detection
  • Prototype pollution prevention

How to use:

export const action = secureServerAction(
async (data: Data) => {
// Your logic here
},
{
sanitizeInputs: true,
requireCSRF: false,
}
);

Related Documentation: