Skip to main content
interlace
Plugin: openai-securityRules

no-untrusted-content-in-prompt

Disallow untrusted content built into the OpenAI system prompt

Disallow untrusted content built into the OpenAI 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

instructions and additional_instructions, a role: 'system' or role: 'developer' turn in messages (Chat Completions) or input (Responses API), content-part arrays, and the Agents SDK's new Agent({ instructions }) / new RealtimeAgent(โ€ฆ). Request paths: chat.completions.create / parse / stream / runTools, responses.create / parse / stream, and the Assistants API's assistants.create / update and threads.runs.create / createAndPoll / createAndStream / stream.

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 (instructions: 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 OpenAI'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 OpenAI 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

const client = new OpenAI();
await client.responses.create({
  instructions: `You are an assistant for ${tenantName}.`,
});

Correct

const client = new OpenAI();
await client.responses.create({
  instructions: 'You are a helpful assistant.',
  input: [{ role: 'user', content: `Tenant: ${tenantName}` }],
});

Use this rule

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

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