System prompt concatenates unbounded user content

ID

unbounded-user-content-in-system-prompt

Severity

high

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

OWASP LLM

LLM01:2025 — Prompt Injection

Family

LLM01 — Prompt Injection

Red-team vectors

Prompt Injection, Jailbreaks

Tags

ai_security

Description

A role=system prompt that string-concatenates a user-controlled variable directly into its body — with no role separation, delimiter, or input guardrail — lets attacker-supplied text override the system instructions. This is the canonical prompt-injection exposure (OWASP LLM01).

This detector raises a finding when a system-role message interpolates a user-controlled variable (an f-string / format placeholder such as {user_input}, or a "…​" + user_input concatenation) within the same construction window.

When a recognized input guardrail — llm_guard / scan_prompt, a NeMo Guardrails LLMRails / RailsConfig wrapper, input_rail, sanitize_input — is used near the flagged code, the finding is still reported but with confidence lowered to low, and the guardrail_hint property names the guardrail found. A guardrail nearby does not guarantee that the user content reaching this system prompt is actually protected, so review these low-confidence findings and mute them once you confirm the guardrail covers the input path. Guardrail names that only appear in comments or string literals do not lower the confidence.

A template-escaped placeholder ({{user_input}}, rendered through a template with role separation) is considered safe and is not flagged.

Examples

messages = [
  {"role": "system", "content": f"You are a helpful assistant. User says: {user_input}"}, (1)
]
openai.chat.completions.create(model="gpt-4o", messages=messages)
1 User-controlled {user_input} is concatenated straight into the system role, so a hostile input can rewrite the agent’s instructions.

Mitigation / Fix

sanitized = llm_guard.scan_prompt(user_input)                     (1)
messages = [
  {"role": "system", "content": "You are a helpful assistant."}, (2)
  {"role": "user", "content": sanitized},                         (3)
]
1 Run an input guardrail on the user content and use its sanitized output — scanning without using the result protects nothing.
2 Keep the system role free of user input.
3 Pass the guarded user content as a separate user-role message (role separation).