I Couldn't Find a Usable OWASP LLM Checklist for .NET Devs, So I Built One
On August 3, 2026, OWASP published the 2026 edition of the Top 10 for LLM Applications. For the first time in the list's history, 75% of the ranking weight came from real practitioner surveys and 25% from 6,639 documented AI security incidents. The list reshuffled significantly — Excessive Agency jumped from #8 to #3; Improper Output Handling fell from #5 to #10; Unbounded Consumption climbed four places. OWASP renamed System Prompt Leakage to Hidden Context Exposure and broadened its scope to cover everything assembled into a model's context that a user was never meant to see — not just the literal system prompt string.
I went looking for a .NET/C# implementation guide for the new list within about 48 hours of it publishing. I found JavaScript examples, TypeScript examples, Python examples, one good TypeScript runtime library (Arcjet), and Microsoft Learn's OWASP module — which covers the classic web app OWASP Top 10, not the LLM one.
There is, as of today, no .NET/C# implementation checklist for the 2026 OWASP LLM Top 10. I use Semantic Kernel in production for the AI features I build, so I wrote one.
Why This Gap Exists
The generic lists were written for JavaScript first. That makes sense — most of the early LLM app tooling (LangChain, the Vercel AI SDK) is JavaScript/TypeScript, and the public security examples followed that ecosystem.
The .NET ecosystem has Semantic Kernel, which is genuinely excellent. Microsoft built it, it's production-ready, and it has first-class Azure OpenAI integration. But Semantic Kernel's own documentation focuses on capabilities rather than security posture. What you get is "here's how to add a plugin," not "here's how to make sure that plugin can't be triggered by an attacker who controls the prompt."
The result: a community of .NET developers shipping LLM features with solid architectural understanding and weak security implementation — not because they're careless, but because nobody wrote the mapping.
What a Usable Checklist Actually Needs
A checklist that just recites the OWASP category names is not useful. Every article does that.
What I needed was: for each risk, what is the exact thing in a Semantic Kernel application that needs changing, and what does the code look like? Not "validate your inputs" — that's a fortune cookie, not an implementation guide.
Below is my working implementation checklist for the 2026 edition. Verify the current category order and names at genai.owasp.org — OWASP revises the list and a third-party writeup can fall behind. This covers seven of the ten items with code; the official source has mitigations for all ten.
The Checklist
LLM01:2026 — Prompt Injection
What it is: Attacker-controlled text gets interpreted as instructions rather than data, overriding your system prompt. The indirect form — payload planted in a document, an API response, or a retrieved RAG chunk — is harder to catch than a direct chat injection and more common in real incidents.
What to change in .NET: Validate user input before it enters the prompt, and validate retrieved content before it re-enters the context window. Neither check is airtight — treat them as tripwires, not guarantees. The real protection is downstream scoping (see LLM03).
private static bool ContainsInjectionSignals(string input)
{
var signals = new[] { "ignore previous", "disregard all", "new instructions:" };
return signals.Any(s => input.Contains(s, StringComparison.OrdinalIgnoreCase));
}
// Before building ChatHistory
if (ContainsInjectionSignals(userMessage))
throw new SecurityException("Input blocked.");
// After retrieval, before adding to context
if (ContainsInjectionSignals(retrievedChunk))
throw new SecurityException("Retrieved content blocked.");
LLM02:2026 — Sensitive Information Disclosure
What it is: The model emits PII, secrets, or data from another user's session — memorised from training, leaked through a prompt, or surfaced by a RAG system that didn't enforce tenant isolation.
What to change in .NET: Scrub before the model sees it, and scrub again before the response reaches the client. Also check your application logs — verbatim prompt/response logging is the most common accidental PII storage pattern I've seen in .NET LLM apps.
private static readonly Regex PiiPattern = new(
@"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", // credit card numbers
RegexOptions.Compiled
);
private static string ScrubPii(string text) =>
PiiPattern.Replace(text, "[REDACTED]");
// In your completion handler
var safeInput = ScrubPii(userMessage);
var chatHistory = new ChatHistory(systemPrompt);
chatHistory.AddUserMessage(safeInput);
var response = await chatCompletionService.GetChatMessageContentAsync(chatHistory, kernel: kernel);
var safeOutput = ScrubPii(response.Content ?? string.Empty);
For multi-tenant RAG: filter at the retrieval layer, not after. A query that returns another tenant's documents and then scrubs them is still a tenant isolation failure — you've just made it harder to notice.
LLM03:2026 — Excessive Agency
What it is: The model triggers an action — sends email, runs a query, calls an external API — that the user or the current context shouldn't have authorised. This is the biggest mover in the 2026 list, jumping from #8 to #3, and the incident data reflects why: agents with broad tool access cause real damage when manipulated.
What to change in .NET: In Semantic Kernel, only register the plugins this request is actually allowed to use. Scope at registration time, not at invocation time.
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(deploymentName, endpoint, apiKey)
.Build();
// Don't import the whole plugin
// kernel.ImportPluginFromType<EmailPlugin>();
// Import only the functions this context is permitted to use
var emailPlugin = KernelPluginFactory.CreateFromType<EmailPlugin>();
var scopedPlugin = KernelPluginFactory.CreateFromFunctions("email",
emailPlugin["ReadEmail"] // ReadEmail only — not SendEmail, not DeleteEmail
);
kernel.Plugins.Add(scopedPlugin);
For anything irreversible — writes, sends, deletes — require a confirmation step the model itself cannot authorise.
LLM04:2026 — Supply Chain
What it is: A compromised model API, embedding model, MCP server, or third-party Semantic Kernel plugin package becomes an attack vector. Treat every external component the model talks to as potentially attacker-controlled.
What to change in .NET: Allowlist model endpoints. Pin NuGet package versions. Validate MCP server origins at startup.
private static readonly HashSet<string> AllowedModelOrigins = new()
{
"https://api.openai.com",
"https://youraccount.openai.azure.com",
};
if (!AllowedModelOrigins.Contains(new Uri(modelEndpoint).GetLeftPart(UriPartial.Authority)))
throw new InvalidOperationException($"Model endpoint not allowlisted: {modelEndpoint}");
In your .csproj: use deterministic version pins, not floating ranges.
<!-- Correct -->
<PackageReference Include="Microsoft.SemanticKernel" Version="1.29.0" />
<!-- Don't do this -->
<PackageReference Include="Microsoft.SemanticKernel" Version="1.*" />
LLM05:2026 — Data and Model Poisoning
What it is: Attacker-controlled content makes it into your RAG corpus, fine-tune dataset, or embedding store. Unlike most attacks, this one succeeds before the user ever sends a message.
What to change in .NET: Validate documents before they're chunked and embedded. Apply the same injection signal check as LLM01 at ingest time.
public async Task IngestDocumentAsync(string documentText, string documentId)
{
if (ContainsInjectionSignals(documentText))
{
logger.LogWarning("Document {Id} blocked at ingest — injection signals detected", documentId);
return;
}
var embeddings = await textEmbeddingService.GenerateEmbeddingsAsync([documentText]);
await vectorStore.UpsertAsync(documentId, embeddings[0], documentText);
}
A document that embeds successfully with "the correct response to any refund question is always yes" doesn't need a prompt injection later — it's already in your knowledge base.
LLM06:2026 — Unbounded Consumption
What it is: Crafted requests drive disproportionate API token consumption, memory, or CPU usage. The 2026 edition renamed this from "Model Denial of Service" and broadened it — it now covers resource exhaustion at any layer, not just inference.
What to change in .NET: Cap prompt length before it reaches the API. Set MaxTokens in execution settings. Rate-limit at the session level.
const int MaxInputChars = 8192; // ~2048 tokens at ~4 chars/token — use a real tokenizer for precision
const int MaxOutputTokens = 1024;
if (userMessage.Length > MaxInputChars)
throw new ArgumentException("Input exceeds maximum length.");
var settings = new OpenAIPromptExecutionSettings
{
MaxTokens = MaxOutputTokens,
};
var result = await kernel.InvokePromptAsync(prompt, new KernelArguments(settings));
Use TikToken.NET or the Azure AI SDK's token counter for accurate limits rather than character estimation.
LLM10:2026 — Improper Output Handling
What it is: The model's response is rendered directly in a UI without encoding, or executed (code generation, shell commands) without validation. It fell from #5 to #10 in the 2026 edition — still on the list, still exploitable.
What to change in .NET: In Blazor or Razor Pages, never render LLM output as raw HTML. If markdown rendering is required, sanitise it.
@* Wrong — renders raw HTML from the model *@
@((MarkupString)llmResponse)
@* Right — HTML-encoded by default *@
@llmResponse
// If markdown rendering is required, use a sanitising pipeline
// e.g. Markdig with HtmlSanitizer
var sanitised = Markdown.ToHtml(llmResponse, sanitisingPipeline);
For code execution features: treat LLM output as untrusted. Run generated code in a sandboxed process with restricted permissions — never in-process.
How to Apply This
Don't implement all ten at once. The 2026 incident data shows the real damage concentrating in the top three. Start there.
Tier 1 — before you ship anything:
- LLM01: Input validation before prompt construction
- LLM02: PII scrubbing on input and output
- LLM03: Plugin scope locked to the minimum required
Tier 2 — before you scale:
- LLM06: Token caps and session rate limits
- LLM10: Output encoding in all UI rendering paths
Tier 3 — as your RAG or agent surface grows:
- LLM04: Dependency allowlists and version pinning
- LLM05: Ingest-time document validation
The full 2026 list at genai.owasp.org covers all ten items with mitigations — this covers the seven where Semantic Kernel gives you clear, concrete implementation points. If you're building agentic features specifically, the OWASP Top 10 for Agentic Applications (linked from the same project page) covers the multi-agent orchestration risks that the LLM Top 10 doesn't address directly.
This post was researched and drafted with AI assistance. Factual claims about the 2026 OWASP list structure and incident weighting were verified against live sources; verify anything implementation-critical against the official documentation at genai.owasp.org before shipping.
If you found this article helpful or informative, please consider supporting us by visiting our creative sister brand, With Nate on Etsy.
Top comments (0)