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 version pin (numeric or semver) ⇒ no finding;

  • a label whose 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 / @latest ref 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 version bound 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.