Your Next.js AI Route Is Open to Attack
Next.js Prompt Injection: How to Protect Your AI Features (Vercel AI SDK Included)
Also known as: Next.js AI security, Vercel AI SDK prompt injection, protect Next.js chatbot, App Router security•Affecting: Next.js, Vercel AI SDK, OpenAI, App Router, Pages Router
Next.js API routes and Vercel AI SDK Server Actions pass user input directly to LLMs with no validation by default. This guide covers how to add prompt injection protection to App Router routes, Pages Router handlers, Server Actions, and a global middleware guard.
TLDR
Next.js AI routes are vulnerable to prompt injection because they pass user messages directly to the LLM. Protect them by calling the SafePrompt validation API before any LLM call. For App Router: validate in the route handler. For Vercel AI SDK: validate before streamText(). For global coverage: add middleware.ts that intercepts all AI API routes. Fix time: under 15 minutes per route.
Quick Facts
Why Next.js AI Routes Are a High-Value Target
Next.js has become the dominant framework for shipping AI-powered web applications. The App Router's Server Actions make it trivial to stream responses from OpenAI, Anthropic, or Google directly to the browser. The Vercel AI SDK handles the streaming infrastructure. Neither does anything to validate what the user typed before it reaches the model.
The attack surface is deceptively large. A typical Next.js AI chat application might expose any of the following:
- Route handlers at
/api/chat— POST endpoints that accept a messages array and forward it to OpenAI or another provider. - Server Actions using
streamTextorgenerateText— called directly from Client Components via Vercel AI SDK'suseChathook oruseActions. - Pages Router API routes — traditional
pages/api/handlers doing the same thing in the older routing model.
Every one of these entry points accepts a string from the browser and forwards it, unvalidated, to an LLM that has been given a system prompt, tool access, and the authority to respond on behalf of your application.
What a Default Next.js AI Route Looks Like to an Attacker
This is the default code in the Next.js AI chatbot template. Copy-pasting it deploys an unprotected LLM endpoint.
The Three Attack Surfaces in a Next.js AI App
1. Route Handlers (App Router)
App Router route handlers at app/api/*/route.ts receive the full request body. Most chat implementations destructure a messages array from the body and pass it straight to the LLM. The last user message in that array is the injection vector.
The fix is one async function call before the LLM invocation. Extract the latest user message, call SafePrompt's validation endpoint, check isSafe, and either proceed or return a 400. The entire modification is under 20 lines.
2. Server Actions with Vercel AI SDK
The Vercel AI SDK's useChat hook with Server Actions is a popular pattern because it handles streaming without any custom route infrastructure. The Server Action is called directly from the client, which means the browser's input reaches your server-side code with the same injection risk as a route handler.
For Server Actions, the validation call must happen before streamText() orgenerateText() is called. Streaming creates a race condition if you try to validate mid-stream — validate first, then decide whether to open the stream at all.
3. Middleware — The Global Guard Pattern
Next.js middleware runs before any route handler or Server Action processes the request. For applications with multiple AI endpoints, a single middleware.ts file at the project root can intercept every AI API call and validate it before it reaches any handler.
The trade-off: middleware runs at the Edge runtime by default, which means you need to be aware of cold start latency and that the SafePrompt validation call adds to the request chain. For most applications this is acceptable. For latency-critical scenarios, per-route validation in the handler is preferable.
| Approach | Coverage | Setup | Latency Impact | Best For |
|---|---|---|---|---|
| Per-route validation | Each route you protect manually | Per route, ~15 min each | Minimal | Targeted protection, fine-grained control |
| Server Action guard | Per action | Per action, ~10 min | Minimal | Vercel AI SDK useChat apps |
| middleware.ts global guard | All matched routes at once | One file, ~20 min total | Edge call overhead | Apps with many AI endpoints |
Implementation Examples
The three tabs below cover App Router route handlers, Vercel AI SDK Server Actions, and the global middleware guard pattern. Choose the one that matches your architecture, or combine them for defense in depth.
import { NextRequest, NextResponse } from 'next/server'
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
const SAFEPROMPT_API_KEY = process.env.SAFEPROMPT_API_KEY!
const SAFEPROMPT_URL = 'https://api.safeprompt.dev/api/v1/validate'
interface SafePromptResult {
isSafe: boolean
score: number
threats: string[]
recommendation: string
}
async function validatePrompt(prompt: string): Promise<SafePromptResult> {
const response = await fetch(SAFEPROMPT_URL, {
method: 'POST',
headers: {
'X-API-Key': SAFEPROMPT_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ prompt }),
})
return response.json()
}
export async function POST(req: NextRequest) {
const { messages } = await req.json()
// Extract the latest user message
const lastUserMessage = messages
.filter((m: { role: string }) => m.role === 'user')
.at(-1)?.content ?? ''
// Validate before the LLM ever sees it
const validation = await validatePrompt(lastUserMessage)
if (!validation.isSafe) {
console.warn('[SafePrompt] Blocked:', {
threats: validation.threats,
score: validation.score,
})
return NextResponse.json(
{ error: 'Message blocked due to policy violation.' },
{ status: 400 }
)
}
// Safe — proceed with the OpenAI call
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
stream: false,
})
return NextResponse.json({
content: response.choices[0].message.content,
})
}The Vercel AI SDK useChat Hook — What You Need to Know
The useChat hook from ai/react sends messages to your Server Action or route handler automatically. By default, it sends the full conversation history. Your validation only needs to target the most recent user message — the one the attacker controls.
// messages format from useChat:
// [{ role: 'user', content: 'Hello' }, { role: 'assistant', content: 'Hi' }, ...]
const lastUserMessage = messages
.filter((m) => m.role === 'user')
.at(-1)?.content ?? ''
// Validate this string with SafePrompt before anything elsePrevious messages in the conversation were already validated when they were first submitted. You only need to re-validate the new user input on each turn, which keeps the per-request validation cost to one API call.
Streaming Responses and Validation Timing
Streaming responses require careful placement of the validation call. If you start a stream and then discover the input was malicious mid-stream, you have already sent headers and potentially partial content to the client. The only way to abort cleanly is to validate before the stream opens.
Correct Order for Streaming Routes
- 1. Parse request body and extract user message
- 2. Call SafePrompt validation —
await validate(userMessage) - 3. If
isSafe === false: return 400 response immediately, no stream - 4. If
isSafe === true: create stream and callstreamText()
Reversing steps 2 and 4 breaks this — you cannot reliably close a stream that has already sent response headers.
What Attack Prompts Look Like Against a Next.js AI App
These are representative injection attempts that reach Next.js AI routes in production:
Regex-based approaches miss most of these. The base64 example contains no suspicious keywords — only SafePrompt's semantic analysis catches that the decoded payload is an injection attempt. The context injection exploits multiline string handling. The jailbreak framing uses entirely innocent vocabulary.
Handling the SafePrompt Response in Next.js
The validation endpoint returns a structured response you can use to make granular decisions:
{
"isSafe": true,
"score": 0.03,
"threats": [],
"recommendation": "allow"
}{
"isSafe": false,
"score": 0.95,
"threats": ["role_override"],
"recommendation": "block"
}Log the threats array for monitoring. The threat categories include role_override, data_exfiltration, jailbreak, system_prompt_extraction, and indirect_injection. These are useful for identifying which features are being targeted.
Environment Variables and Configuration
Required environment variables
Never prefix with NEXT_PUBLIC_ — the API key must remain server-side only. Middleware, Server Actions, and route handlers all run on the server and can accessprocess.env.SAFEPROMPT_API_KEY directly.
Validation Coverage Across Next.js AI Patterns
| Next.js Pattern | Where to Validate | When to Call SafePrompt |
|---|---|---|
| App Router route handler | Inside POST handler, before LLM call | After req.json(), before openai.chat.completions.create() |
| Vercel AI SDK streamText | Server Action body, before streamText() | Before the streamText() or generateText() call |
| Vercel AI SDK useChat + Route | Inside route handler | Same as App Router route handler |
| Pages Router API | Inside handler, before LLM call | After req.body parse, before LLM call |
| Global middleware | middleware.ts, before routing | On every matched POST to AI routes |
Error Handling and Fail Modes
When integrating a third-party API into your request path, you need a strategy for when that API is unavailable. There are two philosophies:
- Fail-open — If SafePrompt is unreachable, allow the request to proceed. This maintains availability at the cost of losing injection protection temporarily. Appropriate for low-sensitivity consumer applications.
- Fail-closed — If SafePrompt is unreachable, block the request with a 503. This maintains security at the cost of availability during outages. Appropriate for enterprise applications, financial tools, or any AI with sensitive data access.
Implement whichever matches your risk tolerance. The middleware example above uses fail-closed. Wrap the validation call in a try/catch and decide what to return in the catch block.
Latency Budget
SafePrompt validation typically completes in under 100ms. For a streaming chat application where the first token from OpenAI gpt-4o arrives in 300-600ms, the validation call adds minimal perceptible latency. Users experience a slightly longer pause before the stream begins — a worthwhile trade-off for protection against injection attacks.
Step-by-Step: Protecting an Existing Next.js AI App
- Get your API key. Sign up at safeprompt.dev. The free tier provides 1,000 validations per month.
- Add the key to
.env.local.SAFEPROMPT_API_KEY=your_key. Never prefix withNEXT_PUBLIC_. - Identify your AI entry points. Search for
openai.chat,streamText,generateText, and similar calls across your codebase. Each callsite is a potential entry point. - Add validation before each LLM call. Copy the route handler example and adapt it to your request shape.
- Add logging for blocked requests. The
threatsarray tells you what is being attempted against your application. - Test with known attack prompts. Use the SafePrompt playground to confirm your integration catches real injection attempts.
Protect Your Next.js AI App
- 1. Sign up at safeprompt.dev/signup
- 2. Add
SAFEPROMPT_API_KEYto.env.local - 3. Copy the route handler example from the tab above
- 4. Test with injection prompts in the playground
Further Reading
- LangChain Prompt Injection — Protecting LangChain chains and agents with the same pattern
- What Is Prompt Injection? — Fundamentals of the attack class
- Why Regex Fails at Prompt Injection Detection — Why pattern matching is not enough
- OWASP Top 10 for LLM Applications — The full AI security risk landscape
- SafePrompt API Reference — Full endpoint documentation