OWASP Top 10 for LLMs 2026 and the Agentic Top 10, with Code (and What I Actually Run in Production)
Table of Contents
OWASP now has two lists for AI, and they don’t compete. The Top 10 for LLM Applications 2026 covers the model when it is a component of your app. The Top 10 for Agentic Applications (ASI01-ASI10) covers the moment the model starts acting on its own. The 2026 edition of the first list adds no categories: it reorders the same ten, and the change that matters is Excessive Agency climbing from 6th to 3rd. Below, every risk comes with a realistic failure and its fix in Laravel or TypeScript, and at the end, what I actually run in a system with agents in production.
The sentence that sums up both lists is in the project leads’ letter: “Stop trying to build a model that cannot be fooled. Build the system around it, so that when the model is fooled, and it will be, nothing important breaks.” Everything that follows is a concrete way of doing that.
Two lists, one boundary
- OWASP Top 10 for LLM Applications 2026. OWASP’s site dates it August 3, 2026, and the official announcement came on September 1. It runs 122 pages under CC BY-SA 4.0, and for the first time the ranking isn’t expert votes alone: votes weigh 75% and a corpus of 6,639 classified real-world incidents weighs 25%.
- OWASP Top 10 for Agentic Applications for 2026. Published December 9, 2025 with the “2026” label, and not revised since. It introduces the principle of least agency: don’t grant autonomy where it isn’t needed, because it widens the attack surface without adding value.
OWASP draws the line like this: “This list owns the risk when the model is a component inside your application. The moment that model becomes an actor […] the risk moves to the OWASP Agentic Top 10.” A chatbot answering from your knowledge base lives in the first list. An agent that reads email, decides and calls tools lives in both.
What changed from 2025 to 2026
| 2026 rank | Risk | In 2025 |
|---|---|---|
| LLM01 | Prompt Injection | 1st (=) |
| LLM02 | Sensitive Information Disclosure | 2nd (=) |
| LLM03 | Excessive Agency | 6th (up 3) |
| LLM04 | Supply Chain | 3rd |
| LLM05 | Data and Model Poisoning | 4th |
| LLM06 | Unbounded Consumption | 10th (up 4) |
| LLM07 | Misinformation | 9th (up 2) |
| LLM08 | Hidden Context Exposure | 7th as “System Prompt Leakage” (renamed and broadened) |
| LLM09 | Vector and Embedding Weaknesses | 8th |
| LLM10 | Improper Output Handling | 5th (down 5, the biggest drop) |
A word of caution when searching: several pages titled “OWASP LLM 2026” still list the 2025 order, with System Prompt Leakage at 7 and Unbounded Consumption at 10. The table above comes from the official PDF.
Two points from the letter change how to read the ranking. Prompt injection is still No. 1 by vote, but ranked by recorded incidents alone it drops out of the top 10 entirely: OWASP puts that down to it being so heavily defended that few clean exploits reach public databases. Misinformation is the opposite case: voters put it low and incidents put it high, the biggest gap “in the direction that actually hurts”.
The ten in the LLM Top 10 2026, with code
The examples are short on purpose. Functions with made-up names (fetchAsMarkdown, llm.summarize) are your app’s helpers; the Laravel, laravel/ai SDK and Node APIs are the real ones.
LLM01:2026 Prompt Injection
Any input that changes the model’s behaviour in a way you didn’t intend: the user’s, but also a page the model reads, a tool’s output, an image or audio. The underlying problem, in OWASP’s words: LLMs “make no architectural distinction between “instructions” and “data"". There is no equivalent of a parameterised query, so the defence isn’t a better prompt. It’s letting code decide what goes out.
// ❌ A stranger's comment decides what the brand publishes
$out = $agent->prompt("Reply to this comment: {$comment->text}");
$network->reply($comment, $out['reply']);
// ✅ The model classifies and drafts; code decides whether it ships
$safe = in_array($out['category'], ['gratitude', 'praise', 'greeting'], true)
&& ($out['auto_send'] ?? false) === true
&& ! preg_match('~https?://|www\.|@\w~i', $out['reply']) // no links, no mentions
&& mb_strlen($out['reply']) <= 280
&& ! $comment->isDirectMessage();
$safe
? $network->reply($comment, $out['reply'])
: $comment->update(['suggested_reply' => $out['reply']]); // everything else goes to a person
LLM02:2026 Sensitive Information Disclosure
Confidential data leaving through a channel it shouldn’t. The 2026 edition stresses that the channel “is not only the final answer”: tool-call arguments, reasoning traces, logs and embeddings count too. The cheapest defence is not putting into the context what the task doesn’t need.
// ❌ The whole customer record goes into the context (and the provider's logs)
await llm.summarize(JSON.stringify(customer));
// ✅ Minimisation: only the fields the task needs
const { plan, lastTicketSubject } = customer;
await llm.summarize(JSON.stringify({ plan, lastTicketSubject }));
LLM03:2026 Excessive Agency
The biggest climber. It’s giving the model more functionality, permissions or autonomy than the task requires, so that an unexpected or manipulated output turns into a harmful action, “regardless of what is causing the LLM to malfunction”. Whether the model hallucinated or was tricked doesn’t matter: the permission is what does the damage.
// ❌ A "RunSql" tool that accepts arbitrary SQL
// ✅ A narrow tool, bound to the authenticated user, plus a step cap
#[MaxSteps(5)]
class SupportAgent implements Agent, HasTools
{
public function tools(): iterable
{
return [new OrderStatus($this->user)];
}
}
class OrderStatus implements Tool
{
public function __construct(private User $user) {}
public function description(): string
{
return 'Status of one of the current user\'s orders.';
}
public function schema(JsonSchema $schema): array
{
return ['order_id' => $schema->integer()->required()];
}
public function handle(Request $request): string
{
// Authorisation lives in code, not in the prompt
return $this->user->orders()->findOrFail($request['order_id'])->status;
}
}
LLM04:2026 Supply Chain
The integrity of everything you didn’t write: models, adapters, conversions, packages. What’s specific to MCP servers and tool registries moves to the agentic list (ASI04). The textbook case is from September 2025: the unofficial postmark-mcp package shipped fifteen clean versions, then in 1.0.16 added one line that BCC’d every email to an attacker’s domain.
// ❌ With a range, `npm install` would have pulled the malicious 1.0.16
"postmark-mcp": "^1.0.0"
// ✅ Exact version, lockfile, `npm ci` in CI, and read the diff before upgrading
"postmark-mcp": "1.0.15"
LLM05:2026 Data and Model Poisoning
Someone slips in manipulated data or artifacts to bias the model or plant a backdoor: in training, fine-tuning, embeddings or a RAG knowledge base. In an ordinary app, the most common door is letting what a visitor writes flow straight into what the bot will use to answer everyone else.
// ❌ A visitor's "correction" goes straight into the knowledge base
KnowledgeEntry::create(['body' => $request->input('correction')]);
// ✅ Quarantine with provenance; only a person promotes it
KnowledgeEntry::create([
'body' => $request->input('correction'),
'status' => 'pending',
'source' => 'visitor',
'source_ip' => $request->ip(),
]);
LLM06:2026 Unbounded Consumption
Uncontrolled inference: denial of service, model cloning, or denial of wallet. What sets it apart is cost asymmetry: a request costs the attacker nothing and costs you tokens. OWASP says per-request rate limiting “is no longer sufficient” and asks for hard spending caps. The most common mistake is limiting on a key the client chooses.
// ❌ Throttle keyed on what the client sends: a fresh id per request walks right through
RateLimiter::attempt('chat:'.$request->input('session_id'), 20, fn () => $this->answer($request));
// ✅ Caps that don't depend on the client: per site per day, and tokens per reply
if (! RateLimiter::attempt("chat:site:{$site}:".now()->toDateString(), 300, fn () => true, 86400)) {
return $this->cannedReply(); // fixed reply, no model call
}
// and on the agent: #[MaxTokens(1500)]
LLM07:2026 Misinformation
A false but credible output that someone acts on, be it a person, a workflow or another agent: “The core risk is that the incorrect output is trusted and acted upon.” If your system cites sources, the defence is checking each citation against its source, not asking the model to be careful.
// ✅ A citation only counts if the literal passage is on the page, re-read
const page = await fetchAsMarkdown(claim.url); // deterministic conversion, no LLM
if (!normalize(page).includes(normalize(claim.passage))) rejected.push(claim);
LLM08:2026 Hidden Context Exposure
It used to be System Prompt Leakage, and now covers all hidden context: instructions, retrieved policies, tool schemas. The design rule is to assume all of it can be extracted. The prompt must carry no secrets and must not act as an authorisation boundary.
// ❌ A secret and a business rule in the system prompt
return "Use token {$crmToken}. If the user says VIP2026, apply a 30% discount.";
// ✅ The prompt is public by design; the server validates the discount
return 'You are the support assistant. Answer only from the knowledge base.';
// ...
$discount = Coupon::validFor($user, $code);
LLM09:2026 Vector and Embedding Weaknesses
The risks in the similarity layer: RAG, vector memory, semantic caches. OWASP puts it in one line: “Poisoning makes the system wrong, inversion makes it leak, jamming makes it silent, and access-control failure makes it indiscriminate.” The access failure is the easiest one to commit: search the whole index and filter afterwards.
// ❌ Similarity over the WHOLE index, tenant filter applied later in the app
// ✅ The tenant filter goes INSIDE the vector query (pgvector)
await pool.query(
'SELECT id, body FROM chunks WHERE tenant_id = $1 ORDER BY embedding <=> $2 LIMIT 5',
[tenantId, toSql(queryEmbedding)],
);
LLM10:2026 Improper Output Handling
Passing model output to another component without validating or escaping it: XSS, SSRF, remote code execution. It’s the biggest faller in the ranking, yet there’s a very Laravel trap. Str::markdown() isn’t safe for LLM text out of the box, because CommonMark defaults html_input to allow and allow_unsafe_links to true. Another route the 2026 edition details is image exfiltration: the model writes  and the browser loads it on its own.
{{-- ❌ CommonMark's defaults let raw HTML and javascript: links through --}}
{!! Str::markdown($llmAnswer) !!}
{{-- ✅ --}}
{!! Str::markdown($llmAnswer, ['html_input' => 'strip', 'allow_unsafe_links' => false]) !!}
// ✅ On the client: sanitise what gets rendered, plus a CSP with a strict img-src
el.innerHTML = DOMPurify.sanitize(await marked.parse(answer));
The ten in the agentic list (ASI01-ASI10)
This list starts where the model stops answering and starts acting: it plans over several steps, calls tools, and sometimes talks to other agents.
ASI01 Agent Goal Hijack
Content the agent reads (a page, an email, a tool’s output) changes its goal or plan. Unlike LLM01, the damage doesn’t stay in one reply: it carries across every step. OWASP’s example is EchoLeak (CVE-2025-32711): a single email, with no click from anyone, led Microsoft 365 Copilot to exfiltrate internal data. The structural defence is that the agent reading third-party content has no outbound tools.
// ✅ The agent that reads third-party web pages can't send anything anywhere
const readerTools = [search, readPage]; // no sendEmail, writeFile or httpPost
const result = DossierSchema.parse(JSON.parse(agentOutput)); // zod: only the expected shape
ASI02 Tool Misuse & Exploitation
The agent uses a legitimate tool, within its permissions, in a harmful way: deletes, abuses an expensive API, exfiltrates. In MCP, annotations like readOnlyHint help, but the spec says clients must treat them as untrusted. The only guarantee is that the dangerous tool doesn’t exist.
server.registerTool(
'list_posts',
{
description: 'Lists posts (read-only)',
inputSchema: { brand: z.string() },
annotations: { readOnlyHint: true },
},
async ({ brand }) => ({ content: [{ type: 'text', text: JSON.stringify(await posts.list(brand)) }] }),
);
// And no 'publish_post' gets registered
In Laravel MCP, the equivalents are the #[IsReadOnly] and #[IsDestructive] attributes.
ASI03 Identity & Privilege Abuse
Inherited or shared credentials, unbounded delegation chains, and agents with no identity of their own, so nobody can attribute who did what. The GitHub MCP case (Invariant Labs, May 2025) is exactly this: a malicious issue in a public repo led the agent to leak private repos, because its token reached all of them.
// ❌ The agent uses an admin token
// ✅ One token per agent and purpose, with the smallest ability
$token = $bot->createToken('ingest-agent', ['licitaciones:ingest'])->plainTextToken;
abort_unless($request->user()->tokenCan('licitaciones:ingest'), 403);
ASI04 Agentic Supply Chain Vulnerabilities
Same as LLM04, but live: a third-party MCP server’s tools load when the agent starts, and their descriptions can change overnight to inject instructions (tool poisoning). If you depend on someone else’s MCP, pin its fingerprint.
// ✅ If tool descriptors change, they aren't used until someone reviews them
const { tools } = await client.listTools();
const hash = createHash('sha256').update(JSON.stringify(tools)).digest('hex');
if (hash !== APPROVED_TOOLS_HASH) throw new Error('MCP descriptors changed: review before use');
ASI05 Unexpected Code Execution
The agent generates or runs code (shell, eval, package installs) and ends up compromising the host. The clearest case is GitHub Copilot’s CVE-2025-53773: an injection got agent mode to write the setting that auto-approves every command into .vscode/settings.json. The agent was editing its own configuration.
// ❌ exec(`git log ${modelArg}`) → a shell plus the model's text
// ✅ Fixed binary, arguments as an array, no shell, and an allowlist
if (!['log', 'status'].includes(sub)) throw new Error('Subcommand not allowed');
execFile('git', [sub, '--oneline', '-n', '20'], { cwd: repoDir, shell: false }, cb);
ASI06 Memory & Context Poisoning
Poisoning persistent memory, summaries or RAG to bias future sessions. Single-turn injection is LLM01; this is the kind that stays.
// ❌ Global memory written from whatever anyone said
Memory::create(['fact' => $out['learned']]);
// ✅ With an owner, provenance and expiry; never from untrusted content
Memory::create([
'user_id' => $user->id,
'fact' => $out['learned'],
'source' => 'chat',
'expires_at' => now()->addDays(30),
]);
ASI07 Insecure Inter-Agent Communication
Messages between agents without authentication, integrity or replay protection. If one agent takes orders from another, those orders must be signed and must expire.
const sig = createHmac('sha256', key).update(`${msg.id}.${msg.exp}.${msg.body}`).digest();
if (Date.now() > msg.exp || !timingSafeEqual(sig, Buffer.from(msg.sig, 'hex'))) reject(msg);
ASI08 Cascading Failures
Not the origin of the failure but its propagation: a hallucination or a malicious input gets amplified from agent to agent, or turns into a retry storm. The defence is ceilings and idempotent jobs.
// ✅ A ceiling per run and per window: every row is an AI call
$pending = Interaction::whereNull('category')
->where('created_at', '>', now()->subHours(48))
->limit(20)
->get();
ASI09 Human-Agent Trust Exploitation
Exploiting a person’s trust in the agent so that they perform the final action, with their name on it. The defence is having the person approve the real thing (the exact text, the exact recipient), not the agent’s summary of it. And a detail people forget: if the approval link acts on a GET, the email scanner that previews links approves it.
// ✅ The GET shows the real post; only the POST acts
Route::middleware('signed')->group(function () {
Route::get('/approve/{post}', [ApprovalController::class, 'confirm']);
Route::post('/approve/{post}', [ApprovalController::class, 'approve']);
});
ASI10 Rogue Agents
The agent drifts out of its role: goal drift, workflow hijacking, gaming the metric. There’s no patch for that, there’s a habit: don’t believe the agent’s report.
// ❌ Trusting "200 closed, 0 failures"
// ✅ After a batch write, re-read the state with another instrument
$closed = Task::whereIn('id', $ids)->where('status', 'closed')->count();
if ($closed !== count($ids)) {
alert("The agent said ".count($ids)." and there are {$closed}");
}
That example isn’t made up. It happened to me: an agent reported “200 closed, 0 failures”, and re-reading the database showed it had closed 177 of 198.
What I run in production
This site’s backend runs AI agents every day: they write social copy, classify comments, research and draft articles, and staff the support chat. Here’s how it measures up against both lists.
- Least privilege (LLM03, ASI02, ASI03). The MCP server I use to operate the system from an agent cannot publish, approve, reject or reply to anyone: those tools don’t exist. It can reschedule, but it can’t pull an already-queued post closer than 30 minutes. It can compose a post, but refuses to generate one for a brand that doesn’t require approval, because that would be publishing through the back door. The engagement agent only drafts replies for a person to read. The public-tenders API uses a token with a single ability.
- Human approval (ASI09). Nothing goes out without a person approving it from a signed link, where the GET shows and the POST acts. The admin panel login follows the same pattern, with a one-time code rather than a magic link.
- Checked citations (LLM07). The pipeline that writes articles separates research from drafting and audits every citation by reopening the source. The page is converted to text deterministically, with no model in between, so the auditor reads exactly the same thing. I built it that way because I measured that an engine researching and drafting in one go left 29% to 43% of its claims unsupported.
- Caps that don’t depend on the client (LLM06). The public chat has limits per site per day, per conversation, and on tokens per reply. Hitting one returns fixed text without calling the model. This came out of my own audit: the per-minute limit was keyed on an identifier the client chooses, and 25 out of 25 requests got through.
- Minimal context (LLM02, LLM08). The chat only sees the site’s knowledge base. The prompt holds no credentials, and authorisation lives in code.
- Hostile content (LLM01, ASI01). Agents that read third-party sites are told that no page can give them orders, but above all they return fixed-shape JSON, not actions, and the reader can only reach the public web. This isn’t theoretical: a video-game wiki served two different agents blocks of instructions hidden in the page, and neither one acted on them.
What’s missing, said plainly: the AI usage ledger tracks real spend, but there’s no global money cap that cuts off on its own. And items like LLM05, LLM09, ASI06 and ASI07 don’t apply because there’s no fine-tuning, no RAG, no persistent memory and no agents talking to each other. Not having them is part of the defence: least agency applies to architecture too.
Six real cases
- EchoLeak, CVE-2025-32711 (June 2025). Zero-click prompt injection in Microsoft 365 Copilot: one email was enough to pull internal data. CVSS 9.3, patched server-side. Analysis.
- GitHub MCP (Invariant Labs, May 26, 2025). An issue in a public repo got the agent to leak private repos through a pull request. The flaw is architectural: a token that reaches everything. Report.
- postmark-mcp 1.0.16 (September 25, 2025). The first documented malicious MCP server: a BCC of every email to an attacker’s domain, around 1,500 downloads in a week. Bleeping Computer.
- GitHub Copilot, CVE-2025-53773 (patched August 2025). Agent mode could switch on command auto-approval by editing its own configuration. Embrace The Red.
- Amazon Q Developer for VS Code, CVE-2025-8217 (July 2025). A poorly scoped GitHub token let someone slip a destructive prompt into version 1.84.0; it never ran because of a syntax error. AWS bulletin.
- Gemini in Chrome, CVE-2026-0628 (Unit 42, March 2026). Not a prompt injection but an isolation flaw: an extension with basic permissions could inject code into the Gemini panel and reach the camera and microphone. A reminder that the assistant is also browser attack surface. Unit 42.
A checklist to take with you
- The model proposes; code decides what runs, what gets published and to whom.
- Every tool as narrow as its task, and the one you don’t need shouldn’t exist.
- One token per agent and per purpose.
- Cost caps that don’t depend on anything the client sends.
- Escape model output like any user input, markdown included.
- Exact package versions, and a fingerprint for third-party MCP tools.
- A person approves the real thing, with a GET that shows and a POST that acts.
- After an agent writes in bulk, re-read the state.
Frequently asked questions
When was the OWASP Top 10 for LLMs 2026 published?
OWASP's site dates the resource August 3, 2026 (already August 4 in UTC), and the official announcement came on September 1, with a press release on the 2nd. The PDF itself carries no date on its cover. The agentic list, by contrast, is from December 9, 2025.
What changed in the OWASP Top 10 for LLMs 2026 compared with 2025?
There are no new categories: the same ten are reordered. Excessive Agency climbs from 6th to 3rd, Unbounded Consumption from 10th to 6th, Misinformation from 9th to 7th, Improper Output Handling drops from 5th to 10th, and System Prompt Leakage is renamed Hidden Context Exposure. The ranking also weighs real incident data (25%) alongside expert votes (75%) for the first time.
What is the difference between the OWASP LLM Top 10 and the Agentic Top 10?
The LLM list covers risk when the model is a component of your application, such as a chatbot that answers questions. The agentic list covers the moment the model acts on its own: it plans over several steps, calls tools or talks to other agents. An agent with tools is exposed to both lists.
Can prompt injection be prevented with a better prompt?
No. Models don't distinguish instructions from data, so there is no equivalent of a parameterised query. The defence is architectural: code decides which actions run, the agent that reads third-party content has no outbound tools, and anything sensitive goes through a person.
Is Laravel's Str::markdown safe to use with LLM output?
Not as it comes. CommonMark defaults html_input to allow and allow_unsafe_links to true, so raw HTML and javascript: links get through. Pass the options html_input strip and allow_unsafe_links false, and add a CSP with a strict img-src on the client to block image-based exfiltration.
Found it useful? Get the next one by email
Once a week: what breaks when you upgrade, AI for developers and what I'm building, with sources. No spam.
By subscribing you accept our privacy policy.