Every team that has plugged a model into a product has quietly added an internet-facing service. Almost nobody counts it as part of the attack surface.
Count the ways a normal endpoint fails: no authentication, unbounded input, no rate limit, no audit trail, cost borne by someone else. Now look at how most teams integrated their model. A server route takes a JSON body, forwards it to a provider with a key from an environment variable, and returns the text. That is an unauthenticated proxy to a paid service, dressed as a feature.
We find this in reviews constantly. It is not a rare mistake. It is the default.
Why the usual controls do not apply
The standard endpoint checklist assumes a few things that fail here.
Authorization is not a role check. The endpoint may require a session. But the question is not "is this user logged in." It is "may this user spend $40 of inference, and may they reach this tool, and may they see this data." Those are three different questions, and most integrations ask none of them. Input validation has no schema. You cannot write a Zod schema for natural language. The input is unbounded by design. What you can bound is the length, the attachments, the tools available, and the tokens spent. Most code bounds none of these. Logs are the product. For a normal endpoint, logging the request is cheap and good. For an LLM route, the request is the user's private data, and the response may contain anything. Teams that log everything for debugging build a sensitive data store they never planned to own. The cost surface is new. A single request can burn a dollar. A loop in a client can burn a thousand. Unless you meter per user and cap it, one customer with a bug is a line item on your bill.The proxy problem
The most common finding is the passthrough. The client sends a model name and a message list. The server forwards both. An attacker now chooses the model, the system prompt, the tools, and the length.
In our reviews, we have seen users reach models the product never intended to offer, spend budgets meant for a whole team, and pull system prompts. Every one of those was a passthrough that passed a field it did not need to pass.
The fix is boring. Do not forward fields you did not design. The server chooses the model. The server owns the system prompt. The server validates message count and total length. The client sends a message, not a request.
Tools are the real authorization boundary
Once the model can call tools, the model's output becomes an instruction to your backend. That flips the trust direction. The model is no longer just producing text. It is producing commands.
So the tools need the same checks you would apply to a user. Does this user own this record? Can they run this action? Is this tool reachable without a second factor for destructive steps? If the model can be convinced to call a tool, the tool must assume it is being called by an adversary.
Prompt injection is one path to that. It is not the only one. A confused model with broad tool access is enough.
What to do this week
Inventory every route that touches a model. There are probably more than you think, including the ones in internal tools.
For each one, answer four questions: who can call it, what can it spend, where does the data go, and what can it reach.
Then remove one field from every passthrough. Pick the most dangerous one. For most teams that is the model name.
None of this is exotic. It is the same discipline we apply to any endpoint. The reason it gets skipped is that the model felt like a library, not a service. It is a service. Treat it like one.