{"slug": "prompt-injection-is-a-permissions-problem-not-a-model-problem", "title": "Prompt Injection Is a Permissions Problem, Not a Model Problem", "summary": "A developer argues that prompt injection in LLM-based agents is fundamentally a permissions problem, not a model behavior problem. They propose a capability-based architecture where untrusted content is processed by a component with no credentials or tools, and actions are validated against a schema, making a compromised agent 'boring' and limiting the impact of hostile instructions.", "body_md": "Every mitigation that treats injection as a text-filtering problem eventually fails. Here's the capability-based version that doesn't depend on the model behaving.\n\nEvery prompt injection discussion I read eventually arrives at the same place: better instructions. Put the system prompt in a stronger position. Tell the model to ignore instructions in retrieved content. Add a classifier that detects malicious input.\n\nAll of these help. None of them are a control, because all of them depend on the model behaving correctly on an input someone else chose.\n\nHere's the reframe that made this tractable for me: injection is not an input-validation problem. It's a privilege problem. The question is not \"can something get bad instructions into the context.\" Assume it can. The question is what those instructions are able to reach.\n\nThe actual mechanism\n\nAn LLM has one channel. Your instructions and the data it processes arrive in the same stream, in the same format, with no structural marker separating them. There is no equivalent of a parameterised query — no way to say this part is code and that part is strictly data at the protocol level.\n\nThat's not an implementation gap someone will close next quarter. It's a property of how these models take input.\n\nWhich means the moment your agent reads anything you didn't write — a web page, an email, a PDF, an issue comment, a search result, a filename — that content is instructions-adjacent. Not because the model is naive, but because there is no layer that could reliably tell the difference.\n\nNow stack that against how we build agents: give it tools, give it credentials, let it run unattended. We've built systems that take instructions from anywhere and act with our authority.\n\nThe injection is unavoidable. The authority is a choice.\n\nWhy filtering doesn't get you there\n\nBriefly, because it's the natural first idea:\n\nDetection classifiers are a bounded search problem for whoever's writing the input, and they have to succeed every time while an attacker needs one pass. Delimiters and \"ignore anything below this line\" instructions live in the same channel as the content, so they're addressable by the content. Encoding tricks, multiple languages, and content in images or documents all route around text-level rules.\n\nFiltering is worth having. It reduces volume. It is not a boundary, and building as if it were is where people get hurt.\n\nThe control that actually holds\n\nAssume the model will, at some point, faithfully execute a hostile instruction. Now design so that doing so is boring.\n\nThat's it. Everything below is a way of making a compromised agent boring.\n\nThe single highest-value structural change. One component processes untrusted content and has no credentials and no tools. It returns structured data. A second component, which never sees the untrusted text, acts on that data.\n\npython\n\nagent = Agent(tools=[send_email, read_files, http_get], creds=CREDS)\n\nagent.run(\"summarize [https://example.com/thing](https://example.com/thing) and email me\")\n\nraw = fetch(url) # plain fetch, no model\n\nsummary = reader.extract(raw) # model, NO tools, NO creds\n\n# returns {title, points[], urls[]}\n\nmailer.send(to=OWNER, body=render(summary)) # code path, fixed recipient\n\nA hostile instruction in that page can influence summary. It cannot reach mailer, because mailer isn't reading it and its recipient isn't a variable the model controls.\n\nNotice to=OWNER is hardcoded. The moment the recipient becomes model-determined, you've reconnected the two halves.\n\nDon't let the model invoke. Let it propose, and validate the proposal against a schema you wrote:\n\npython\n\nALLOWED = {\"summarize\", \"tag\", \"draft_reply\"}\n\ndef handle(proposal: dict) -> dict:\n\naction = proposal.get(\"action\")\n\nif action not in ALLOWED:\n\naudit(\"reader\", action, None, False, \"not in allowlist\")\n\nraise PermissionError(f\"refused: {action}\")\n\nargs = SCHEMAS[action].validate(proposal.get(\"args\", {}))\n\nreturn EXECUTORS[action](https://dev.to**args)\n\nAllowlist, never denylist. You can't enumerate everything you don't want; you can enumerate the four things you do.\n\nReading your data is only half an incident. The other half is getting it out. Two things carry it:\n\nOutbound network. If the agent can request arbitrary URLs, every byte it can read can leave via a query string. Allowlist egress by host.\n\npython\n\nALLOWED_HOSTS = {\"api.internal.example\", \"docs.example.com\"}\n\ndef safe_get(url):\n\nhost = urlparse(url).hostname or \"\"\n\nif host not in ALLOWED_HOSTS:\n\naudit(\"agent\", \"http.get\", host, False, \"host not allowed\")\n\nraise PermissionError(f\"blocked host: {host}\")\n\nreturn httpx.get(url, timeout=10)\n\nRendered output. This one catches people. If your agent's output is rendered as markdown or HTML in a UI, an image reference the model was induced to emit will make the browser issue a request — with whatever ended up in the URL. The user sees a broken image. The data is gone.\n\nStrip or proxy remote references in model output. Treat model output as untrusted, because it is: it's downstream of untrusted input.\n\nSplit every capability into a reversible half and an irreversible half, and gate the second:\n\nReversible — let it run Irreversible — human confirms\n\nDraft an email Send it\n\nCreate a branch Merge to main\n\nStage a change Deploy\n\nPropose a delete Delete\n\nPrepare a transaction Sign it\n\nReviewing a draft is fast. Writing one isn't. You lose almost no throughput and you remove the entire class of failures you can't undo.\n\nPer-agent keys. Expiry. Spend cap. Rate limit. Egress allowlist. Logs the agent can't write to, recording denials as well as allows — a spike in refusals is the cheapest signal you'll ever get, and it's the one people forget to record because nothing bad happened.\n\nI've written up the credential architecture in more detail [here]; the short version is one identity per agent per environment, read-only until write is earned, and secrets held by a broker the model can't instruct.\n\nIndividually harmless capabilities compose into dangerous ones:\n\nread mail + send mail → your inbox is a password-reset engine, and now something can both trigger and consume the resets\n\nread files + arbitrary egress → an exfiltration path missing only a trigger\n\nwrite repo + CI → code execution in your build environment, with your build secrets\n\ndelete + logs in the same system → an incident with no forensics\n\nRead across the row per agent, not down the column per permission. Dangerous configurations are almost always horizontal.\n\nThe bit about wallets\n\nIf any of this touches financial rails: dedicated credentials that exist for nothing else, hardware-backed signing so the key never enters the environment the agent runs in, and a human on every signature. Assume any key or seed that has passed through a general-purpose model's context is compromised and rotate it.\n\nEducational only, not financial advice.\n\nThe checklist\n\n[ ] Reader component has no tools and no credentials\n\n[ ] Model returns intents; a schema validates them; an allowlist gates them\n\n[ ] Egress allowlisted by host\n\n[ ] Remote references stripped from rendered model output\n\n[ ] Irreversible actions gated behind a human\n\n[ ] One credential per agent, with expiry + spend cap + rate limit\n\n[ ] Append-only external logs, denials included\n\n[ ] Permission sets audited per agent, across the row\n\n[ ] Kill switch documented and tested once\n\nNone of it depends on the model behaving. That's the whole point.\n\nThe industry keeps looking for the fix that makes injection stop happening. There probably isn't one, for the same reason there's no fix that makes SQL injection stop being attempted — we solved that by removing the ambiguity between code and data at the protocol level, and LLMs don't have a protocol level to do it at.\n\nSo we do the other thing. Assume the instruction lands. Make sure it lands somewhere with nothing to reach.\n\n[YOUR NAME] — I build and break down AI stacks, with a focus on the security side most tool reviews skip: [AiStackGuru](https://www.youtube.com/@AiStackGuru). There's an interactive [AI Visibility Tool](https://digimsm.com/ai-visibility-checker/) that maps what a given permission set exposes.\n\nWhat's the most surprising permission combination you've found in a running agent? I'd like to collect a few.", "url": "https://wpnews.pro/news/prompt-injection-is-a-permissions-problem-not-a-model-problem", "canonical_source": "https://dev.to/msmyaqoob25/prompt-injection-is-a-permissions-problem-not-a-model-problem-59fk", "published_at": "2026-08-19 07:32:44+00:00", "updated_at": "2026-08-19 07:41:35.414046+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "large-language-models"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/prompt-injection-is-a-permissions-problem-not-a-model-problem", "markdown": "https://wpnews.pro/news/prompt-injection-is-a-permissions-problem-not-a-model-problem.md", "text": "https://wpnews.pro/news/prompt-injection-is-a-permissions-problem-not-a-model-problem.txt", "jsonld": "https://wpnews.pro/news/prompt-injection-is-a-permissions-problem-not-a-model-problem.jsonld"}}