
What are the real attack surfaces in an LLM feature?
Prompt injection is the one people name, and it matters, but it is rarely the thing that causes the incident. The incidents come from architecture: an AI service that holds write credentials it did not need, a retrieval layer that does not filter by tenant before it searches, a response parsed with enough trust to end up in a query, and logs that quietly store client data forever because nobody decided otherwise.
Indirect injection is the underrated one. If your model reads a document, an email or a web page a user supplied, that content can carry instructions. Any design where reading untrusted text and holding real permissions happen in the same place is one well-worded PDF away from a bad day.
How do you make unauthorised writes structurally impossible?
By not giving the AI service the ability to write. The service that talks to the model holds no write credentials to your database or storage, so a compromised prompt has nothing to compromise with. Output is review-only by default, surfaced to a human rather than applied automatically, and anything that does eventually write goes through your existing application logic with its existing permission checks.
This is a design decision, not a control you bolt on. It is also the single thing that most often turns a blocked security review into an approved one, because it is verifiable: the credential simply is not there.
How do you stop one tenant seeing another's data?
Tenant context is carried explicitly through every hop, from the application through the gateway to the retrieval layer, and filtering happens before the search rather than after it. A retrieval system that fetches broadly and filters afterwards will eventually leak, because the model has already seen the text.
Then it gets tested adversarially on the real path, not in theory: a request in one tenant's context must not be able to see or affect another's data, and that test lives in your repository so it runs again every time someone changes the code.
How do you stop a wrong answer reaching a customer?
A deterministic gate between the model's answer and your application. Code, not another model: a fixed list of rules, each returning pass or fail, giving the same verdict for the same input every time. Numbers reconcile against the source. Required fields are present. Values come from your allowed list. Prohibited content is absent.
A failed rule is never silently corrected. The request retries once with the failure fed back, and if it fails again the response is returned as rejected with the failing rules named, so your interface shows a needs-review state instead of a confident wrong answer. You also get a negative test suite, so your own reviewer can reproduce every result.
How an engagement runs
| Engagement | What happens | Timeline |
|---|---|---|
| Security review | Your AI path examined end to end: prompt flow, credentials, tenant isolation, output handling, logging. Findings written as testable statements | About one week |
| Hardening | Least-privilege redesign, tenant isolation on the real path, validation gate, failure handling, negative test suite in your repository | Two to three weeks |
| Pre-review support | Preparing the AI layer for a customer's security review or procurement questionnaire | Scoped on a call |
Scope and price are set on a call once we know what the AI layer touches. Work runs on synthetic data, with named limited-privilege access removed at handover.
