Skip to main content
interlace
Plugin: gemini-securityRules

no-untrusted-content-in-prompt

Disallow untrusted content built into the Gemini system prompt

Disallow untrusted content built into the Gemini system prompt.

Why

A system prompt is instruction text. Whatever is spliced into it is read by the model as instructions, not as data โ€” so anyone who controls that value controls the agent's rules. The model has no way to tell "this part of my instructions came from a stranger" once the string is assembled.

This is the same argument as mcp-sdk-security/no-tool-description-injection, one layer down: tool descriptions and system prompts are both model-facing instruction surfaces, and both must be static.

What it reads

systemInstruction as a string, a Content ({ parts: [{ text }] }) or a list of parts โ€” directly on the request (@google/generative-ai) or under config (@google/genai). Request paths: generateContent, generateContentStream, getGenerativeModel, startChat, chats.create, caches.create, live.connect, sendMessage and sendMessageStream.

What counts as static

Text this file fixes is static, and is followed within the file (bounded, never across files):

  • a string literal, an interpolation-free template or tagged template (dedent`โ€ฆ`), and concatenations, [โ€ฆ].join() and string methods over those;
  • a const holding any of the above, a member of a same-file object literal of prompts (PROMPTS[kind]), a ternary between static branches, and a same-file helper whose every return is static;
  • an imported constant (RECOMMENDED_PROMPT_PREFIX) and an environment read โ€” module code and operator configuration, not request data;
  • a value that can only print digits or a fixed format: a date (new Date().toISOString()), a count (.length), arithmetic, a comparison, or a parameter typed number / boolean.

A whole-value identifier the file cannot see into still counts as static โ€” a parameter (systemInstruction: systemPrompt inside a wrapper) or a global. Inside a template or a concatenation the same value is the dynamic half, and that is what gets reported, at the place it is spliced in. A request object or a message list held in a const is read the same way as one written inline.

Not this rule

  • The Vercel AI SDK. generateText({ system }) and streamText({ system }) belong to vercel-ai-security/no-dynamic-system-prompt. This rule gates on Gemini's own request paths, so no line is ever reported by both.
  • User turns. { role: 'user', content: `${input}` } is the remediation, not the bug โ€” a user message is where runtime values belong, because the model reads them as data.

Known limitations

  • The gate is file-level: once the Gemini SDK is imported, any call whose member path matches a request path is inspected, unless this file shows the receiver is something else (an object literal, or a new X() of a class that does not extend the SDK). A receiver that is a parameter, an import or a call result is inspected โ€” clients are passed around, and resolving every one back to its construction would take cross-file analysis.

Incorrect

await ai.models.generateContent({
  systemInstruction: `Answer as ${persona}.`,
  contents,
});

Correct

await ai.models.generateContent({
  systemInstruction: 'You are a helpful assistant.',
  contents: [{ role: 'user', parts: [{ text: `Persona: ${persona}` }] }],
});

Use this rule

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

oxlint-plugin-gemini-security re-exports eslint-plugin-gemini-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.