Skip to main content
interlace
Plugin: vercel-ai-securityRules

no-dynamic-system-prompt

This rule identifies code patterns where system prompts contain dynamic or user-controlled content

Prevents dynamic content in system prompts to avoid agent confusion attacks.

📊 Rule Details

PropertyValue
Typeproblem
Severity🔴 HIGH
OWASP AgenticASI01: Agent Confusion
CWECWE-74: Improper Neutralization
CVSS8.0
Config Defaulterror (recommended, strict)

🔍 What This Rule Detects

This rule identifies code patterns where system prompts contain dynamic or user-controlled content. Dynamic system prompts can lead to agent confusion attacks where the AI's core behavior can be manipulated.

Both spellings of the system-prompt option are checked. AI SDK v7 renamed it to instructions and marks system as deprecated, so the examples below apply identically to instructions:.

❌ Incorrect Code

// Template literal with expression
await generateText({
  system: `You are a ${role} assistant.`,
  prompt: userInput,
});

// String concatenation
await generateText({
  system: 'You are a ' + role + ' assistant.',
  prompt: userInput,
});

// Function call result
await streamText({
  system: getSystemPrompt(agentType),
  prompt: userInput,
});

// Async system prompt
await generateObject({
  system: await fetchSystemPrompt(),
  prompt: userInput,
});

✅ Correct Code

// Static string literal
await generateText({
  system: 'You are a helpful assistant.',
  prompt: userInput,
});

// Static constant
const SYSTEM = 'You are a helpful coding assistant.';
await generateText({
  system: SYSTEM,
  prompt: userInput,
});

// Static template literal (no expressions)
await streamText({
  system: `You are a helpful assistant.
  You can help with coding tasks.`,
  prompt: userInput,
});

⚙️ Options

OptionTypeDefaultDescription
allowStaticTemplatesbooleantrueAllow template literals without expressions

🛡️ Why This Matters

Dynamic system prompts enable attackers to:

  • Modify AI core behavior - Change fundamental instructions
  • Bypass safety measures - Remove content restrictions
  • Inject persistent instructions - Add malicious instructions that persist across conversations
  • Create agent confusion - Make the AI act inconsistently

🔄 Changes in the 2026-10-10 FP/FN audit

  • A template or + chain built only from string constants (const bindings with literal initialisers) and the current date (new Date(), Date.now() and methods on them) is static and no longer reported.
  • A system prompt read straight from the request — destructured from await req.json(), req.body.x, body.x where body = await req.json(), or searchParams.get(...) — is reported with the new message userControlledSystemPrompt, even with no template around it.
  • Options: the documented defaultOptions now apply. Earlier versions read context.options and silently ran a shorter hard-coded list instead.

🔄 Changes in the 2026-10-11 zero-deferral pass

  • The request value is followed through any number of same-file hops: const body = await req.json(); const persona = body.persona as string;, later reassignments, a same-file helper's return (system = pickPersona(body)), and a same-file function whose parameter is fed from the request.
  • A call to a same-file function every return of which is static text (system: buildSystemPrompt()) is static and no longer reported.

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Static Constant from Dynamic Source

Why: Constant assignment from dynamic source is not traced.

// ❌ NOT DETECTED - Constant assigned dynamically
const SYSTEM = process.env.SYSTEM_PROMPT; // Dynamic!
await generateText({ system: SYSTEM, prompt: userInput });

Mitigation: Use hardcoded string literals. Never use env vars for system prompts.

Conditional System Prompt Selection

Why: Conditionally selected prompts may be static but appear dynamic.

// ❌ FALSE POSITIVE RISK - All options are static
const PROMPTS = { admin: 'You are admin.', user: 'You are user.' };
await generateText({ system: PROMPTS[role], prompt: userInput });

Mitigation: Use if/else with literal strings instead of object lookup.

Import from Constants File

Why: Imported values are not analyzed.

// ❌ NOT DETECTED - May be dynamic in source file
import { SYSTEM_PROMPT } from './prompts';
await generateText({ system: SYSTEM_PROMPT, prompt: userInput });

Mitigation: Apply rule to constants files. Use inline string literals.

📚 References

Use this rule

npm install -D oxlint-plugin-vercel-ai-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-vercel-ai-security"],  "rules": {    "vercel-ai-security/no-dynamic-system-prompt": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-vercel-ai-security'],    rules: {      'vercel-ai-security/no-dynamic-system-prompt': 'error',    },  },});

oxlint-plugin-vercel-ai-security re-exports eslint-plugin-vercel-ai-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.