Position

Prompt injection is an authorization problem

You cannot prompt your way out of prompt injection. The fix is to stop giving the model authority it can be talked into using.

The usual framing treats injection as a content problem: filter the input, harden the system prompt, add instructions telling the model to ignore instructions. All of that helps at the margin and none of it holds, because the model cannot reliably distinguish your instructions from text that looks like your instructions.

Treat it as an authorization problem and it becomes tractable. The model proposes an action. Something else decides whether that action is permitted, and it decides using the caller's identity rather than anything in the conversation.

Retrieved content and user messages are untrusted. The model proposes but does not decide. A tool broker checks the caller and scope against policy, which allows, requires confirmation, or denies. Only permitted effects reach writes and payments.
The trust boundary sits between the model and the effect, not around the prompt.

Everything the model reads is untrusted

A retrieved document was written by someone. A support ticket was written by a customer. A web page was written by anyone. Once any of that enters the context, the context is untrusted, and the system prompt is a suggestion sitting in the same buffer as the attack.

# The model proposes; the broker decides. The prompt carries no authority.
def call_tool(proposal: ToolCall, caller: Principal) -> ToolResult:
    tool = registry.get(proposal.name)
    if tool is None:
        return ToolResult.refused("unknown tool")

    # authorization is checked against the caller, never against the
    # conversation, and never against the agent's own identity
    if not policy.permits(caller, tool, proposal.arguments):
        audit.record(caller, tool, proposal.arguments, "denied")
        return ToolResult.refused("not permitted for this caller")

    if tool.requires_confirmation(proposal.arguments):
        return ToolResult.needs_human(summary=tool.describe(proposal.arguments))

    return tool.invoke(proposal.arguments, as_principal=caller)

Scope tools to the caller, not the agent

The most common mistake we see is an agent that authenticates as a service principal with broad rights. Every user of that agent inherits those rights through it, whatever the prompt says. An injected instruction then runs with the union of everyone's permissions.

Pass the caller through. The tool executes as the person who asked, so an injected instruction can only reach what that person could already reach. Injection becomes a nuisance rather than a breach.

Design the blast radius

  1. Classify every tool as read, reversible write, or irreversible effect.
  2. Allow reads freely inside the caller's scope.
  3. Make reversible writes reversible in fact, with a window and a one-step undo.
  4. Put irreversible effects behind a human confirmation that shows what will happen in business terms, not as a serialized tool call.

What to tell your security team

They will ask what stops the model doing something harmful. The answer is not that the prompt tells it not to. The answer is that the model has no capability to do it, because authorization happens after the proposal and outside the model, against an identity the conversation cannot change. That is a claim they can verify, which is why it is the one worth making.

More field notes

Bring us the problem.

Tell us the outcome you are trying to create, what you have already attempted, and where the constraints are.

Contact nuperX