Registry prompt pinned to a mutable label
ID |
prompt-pinned-to-mutable-label |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
OWASP LLM |
LLM01:2025 — Prompt Injection |
Family |
LLM01 — Prompt Injection |
Red-team vectors |
Prompt Injection, RAG Poisoning |
Tags |
ai_security |
Description
A prompt fetched from a registry (LangSmith, Langfuse, …) by a floating / mutable reference — no version pin, or a mutable label such as latest — means a registry compromise (or an unreviewed edit) rewrites production behaviour without a code change. Pinning to an immutable version makes prompt rollout an explicit, reviewable step.
The detector performs AST call analysis over Python and JavaScript/TypeScript sources, inspecting each registry prompt-fetch call. Because it reasons over the parsed call — not a text line — it handles arguments spread across several lines, is not fooled by argument order, and can tell a constant pin from a variable one:
-
an explicit constant
versionpin (numeric or semver) ⇒ no finding; -
a
labelwhose value is a provably-immutable ref (semver / commit SHA / numeric version) ⇒ no finding; -
a ref carried on the prompt identifier itself (
name:ref/name@ref) is classified the same way —"acme/agent:a1b2c3d4"pins,"acme/agent:latest"does not. This is how the Hub-style SDKs pin, in place of a version argument; -
a
label="latest"(or a:latest/@latestref on the prompt identifier) ⇒ finding; -
a named alias
label(production,prod,staging,main, …) ⇒ finding — a named alias is a mutable pointer a registry compromise can repoint (the mainstream "change prod behaviour" case), so it is not allowlisted; -
no version and no label ⇒ finding (the registry default is a floating head);
-
a
versionbound to a variable (not a constant) ⇒ finding at reduced confidence — it is not a provable immutable pin.
The argument shape differs per language — Python uses keyword arguments (version= / label=); JS/TS carries version as the second positional argument and label inside the options object — but the verdict is identical.
Each finding records the matched prompt_fetch_call, the prompt_registry_provider it belongs to, and why it is mutable (mutable_reason).
The recognised call names come from a versioned, hot-reloadable signature pack (LangSmith pull_prompt/pullPrompt, Langfuse get_prompt/getPrompt, LangChain Hub hub.pull). An entry may keep its qualifier — the pack is matched on the longest dotted suffix — so an SDK whose fetch is named too generically to claim on its own can still be recognised through its module. Teaching the detector a new SDK is a data change — point -Dxygeni.aisecurity.prompt-registry=<file> at an updated pack — not a detector release.
|
This risk is conventionally rated Medium; Xygeni’s 4-level severity scale (info / low / high / critical) has no medium band, so it is reported as high.
|
Examples
Python:
system = langfuse.get_prompt("support.refund_policy") (1)
system = langfuse.get_prompt("support.refund_policy", label="latest") (2)
system = langfuse.get_prompt("support.refund_policy", version=cfg.v) (3)
| 1 | No version pin: the registry serves the floating head, so the prompt can change under the running service. |
| 2 | An explicit latest label is equally mutable. |
| 3 | version bound to a variable is not a provable immutable pin (reported at lower confidence). |
from langchain import hub
prompt = hub.pull("acme-corp/support-agent:latest") (1)
prompt = hub.pull("acme-corp/support-agent:a1b2c3d4") (2)
| 1 | LangChain Hub floating head: :latest is repointed whenever a new revision is pushed. |
| 2 | The Hub’s immutable pin is a commit SHA on the identifier — no finding. |
JavaScript / TypeScript:
const a = client.pullPrompt("support/refund_policy:latest"); (1)
const b = langfuse.getPrompt("support.refund_policy"); (2)
const c = langfuse.getPrompt("support.refund_policy", undefined, { label: "latest" }); (3)
| 1 | LangSmith floating-head suffix on the identifier. |
| 2 | No version and no label — the registry serves the floating head. |
| 3 | A latest label carried in the options object. |
Mitigation / Fix
system = langfuse.get_prompt("support.refund_policy", version=3) (1)
system = langfuse.get_prompt("support.refund_policy", version="v3.1.0") (2)
| 1 | Pin to an immutable numeric version. |
| 2 | Or an immutable semver. A mutable label — latest, but also a named alias like production / staging — is never a safe pin; promote a reviewed version explicitly on rollout. |
const a = langfuse.getPrompt("support.refund_policy", 3); (1)
const b = langfuse.getPrompt("support.refund_policy", undefined, { label: "v3.1.0" }); (2)
| 1 | Pin to an immutable version (second positional argument). |
| 2 | A label is safe only when its value is a provably-immutable ref (semver / commit SHA / numeric); never a mutable alias like latest or production. |