<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Manh Liem</title>
    <description>The latest articles on DEV Community by Manh Liem (@llmrt).</description>
    <link>https://dev.to/llmrt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4122288%2F237b0db5-ae03-4457-91e0-e49fc965c06a.png</url>
      <title>DEV Community: Manh Liem</title>
      <link>https://dev.to/llmrt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/llmrt"/>
    <language>en</language>
    <item>
      <title>Your MCP server is an attack surface. Here is the 30-second check most teams skip</title>
      <dc:creator>Manh Liem</dc:creator>
      <pubDate>Sat, 12 Sep 2026 15:49:09 +0000</pubDate>
      <link>https://dev.to/llmrt/your-mcp-server-is-an-attack-surface-here-is-the-30-second-check-most-teams-skip-105o</link>
      <guid>https://dev.to/llmrt/your-mcp-server-is-an-attack-surface-here-is-the-30-second-check-most-teams-skip-105o</guid>
      <description>&lt;p&gt;MCP (Model Context Protocol) has quietly become the default way LLM apps connect to tools, databases, and internal APIs. That is a good thing. It also means the most common security review of an MCP-enabled app still treats the tool layer like a library call and checks almost nothing about it. I red-team LLM apps for a living, and the MCP layer is where the quiet, unlogged failures happen. Here is the 30-second check I run on every one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MCP is different from the prompt
&lt;/h2&gt;

&lt;p&gt;A prompt is untrusted text. A tool is a capability. When a model calls a tool, it is not producing a string, it is triggering a side effect: a file write, an SQL query, an HTTP request, a payment. The prompt-injection literature focuses heavily on getting the model to &lt;em&gt;say&lt;/em&gt; the wrong thing. The harder problem is getting it to &lt;em&gt;do&lt;/em&gt; the wrong thing, because the action returns a normal-looking result and the surrounding code records a success.&lt;/p&gt;

&lt;p&gt;Three failure modes dominate what I see in the field:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The tool description is the new prompt.&lt;/strong&gt; MCP servers ship with natural-language descriptions of every tool. Those descriptions are read by the model and effectively become part of the instruction surface. If a server description is verbose, ambiguous, or contains an example that is too permissive, it can steer the model toward broader use than the app intended. The check: read every tool description as if it were an attacker wrote it, and ask what the &lt;em&gt;widest&lt;/em&gt; reasonable action the model could take is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The argument boundary is the real boundary.&lt;/strong&gt; The description is only as safe as the argument validation behind it. A tool that accepts a &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;, or &lt;code&gt;url&lt;/code&gt; argument and passes it straight through is a path traversal, injection, or SSRF vector that the model can reach from a single crafted user message. The check: for every string argument that reaches the filesystem, a query builder, or an HTTP client, confirm there is an explicit allow-list or a validated scheme. "We sanitize the model output" is not a boundary, because the model output is exactly the channel the attacker controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The success path is the blind spot.&lt;/strong&gt; When a tool call returns, the app usually logs &lt;code&gt;tool=send_email status=ok&lt;/code&gt; and moves on. The model may have composed a message to the wrong recipient, or included data that should not have been included, and nothing raised. The check: add one assertion on the &lt;em&gt;most sensitive&lt;/em&gt; field of the highest-risk tool (recipient, destination, amount) and fail loudly if it falls outside a narrow expected set. One test is enough to start. The point is to make the quiet failure loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run it without a pentest budget
&lt;/h2&gt;

&lt;p&gt;I package the eight highest-value probes from this checklist as a free endpoint: point it at a tool spec or a prompt, and it returns a short risk report with the specific field it flagged. It is not a substitute for a real review, but it is a 30-second signal that catches the most common MCP mistakes. You can run it here: &lt;a href="https://llmrt-companion.manhliemcn4euwlu.workers.dev/agent-scan" rel="noopener noreferrer"&gt;https://llmrt-companion.manhliemcn4euwlu.workers.dev/agent-scan&lt;/a&gt; . The report is deterministic and the hash of a self-scan is published so you can verify the same input gives the same output.&lt;/p&gt;

&lt;p&gt;If you are building an MCP server, the single highest-leverage thing you can do before shipping is to write down, for each tool, the one field that, if wrong, would be the incident. Then write the one test that catches it. Everything else is hardening.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>mcp</category>
      <category>programming</category>
    </item>
    <item>
      <title>The LLM security failure that doesn't raise an error is the one that costs you</title>
      <dc:creator>Manh Liem</dc:creator>
      <pubDate>Sat, 12 Sep 2026 15:22:32 +0000</pubDate>
      <link>https://dev.to/llmrt/the-llm-security-failure-that-doesnt-raise-an-error-is-the-one-that-costs-you-44c5</link>
      <guid>https://dev.to/llmrt/the-llm-security-failure-that-doesnt-raise-an-error-is-the-one-that-costs-you-44c5</guid>
      <description>&lt;p&gt;I spent a week reviewing LLM apps for a client and found a pattern that surprised me. Every loud failure - a refused prompt, an exception, a blocked tool call - was handled fine. The app had guardrails. The failure that almost caused a real incident was the one that &lt;strong&gt;raised no error at all&lt;/strong&gt;: the model quietly complied with a slightly reworded request, returned a normal-looking response, and the downstream code logged it as a success.&lt;/p&gt;

&lt;p&gt;I am calling these &lt;strong&gt;soft failures&lt;/strong&gt;. They are the expensive kind, because your monitoring never sees them.&lt;/p&gt;

&lt;p&gt;Here is how I think about them, and how I test for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hard failures are easy and soft failures are not
&lt;/h2&gt;

&lt;p&gt;A hard failure breaks a contract. The model says no, the tool call is rejected, the JSON shape is wrong. Your tests catch contract breaks because you can assert on them. You can write &lt;code&gt;assert response is not None&lt;/code&gt;, &lt;code&gt;assert 'refused' in text&lt;/code&gt;, and you will be fine.&lt;/p&gt;

&lt;p&gt;A soft failure keeps every contract intact. The response is valid JSON. The tone is helpful. The tool call is well-formed. The only thing wrong is &lt;em&gt;what&lt;/em&gt; it agreed to do - and that is a semantic property you cannot assert on with a regex.&lt;/p&gt;

&lt;p&gt;When I ran my 8-probe battery against a banking-style assistant, the probe that got flagged was not the aggressive jailbreak. It was the polite, cross-language one. The model did not refuse, did not error, did not look suspicious. It just answered a question it was not supposed to answer. In a log, that row looks identical to a legitimate row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to catch them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Diff the capability surface, not the text.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of asking "does this response look like a leak?", I compare what the model is &lt;em&gt;able&lt;/em&gt; to do before and after a suspected injection. Can it still deny a clearly out-of-scope request? Can it still repeat its constraints back when asked? If a harmless-looking turn makes a previously-denied capability suddenly available, that is a soft failure, and you can detect it structurally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Reproduce the exact bytes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A security finding you cannot reproduce is an opinion. Every probe in my kit ships the raw prompt and the raw model reply, and the report is hash-verifiable (sha256 over the served bytes). This matters because soft failures are probabilistic - a model might comply 30% of the time. If you cannot re-run the exact prompt and compare, you cannot tell a one-off from a real hole.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Treat the score as a gate, not a verdict.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use a 0-100 risk score, but the number is only useful as a CI gate with a fixed threshold you set before you look at results. The moment the score becomes a marketing number, it stops being a decision tool. Deflated expectations, not clever models, is where these audits actually change behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second version
&lt;/h2&gt;

&lt;p&gt;You do not need a full audit to get the main value. A short adversarial battery - eight to fifteen probes covering jailbreak, extraction, indirect injection, and tool abuse - run against your actual system prompt, tells you whether you have any soft failures at all. If the polite probes are flagged and the aggressive ones are not, you know exactly where to invest: the model is not refusing, it is not &lt;em&gt;consistently&lt;/em&gt; refusing.&lt;/p&gt;

&lt;p&gt;I run this as a free sample on my own agent so you can see the shape of the output: paste a system prompt at &lt;code&gt;https://llmrt-companion.manhliemcn4euwlu.workers.dev/agent-scan&lt;/code&gt; and in about 35 seconds you get the risk score, the per-probe raw prompt and reply, and the flags. The report link is durable and the hash is in the response, so you can recompute it later and confirm nothing changed.&lt;/p&gt;

&lt;p&gt;My own production agent scanned at 27/100 MEDIUM on this battery - one flag, the cross-language extraction. I am posting that link because it is the same scan, unedited: &lt;code&gt;https://llmrt-companion.manhliemcn4euwlu.workers.dev/r/e21808b6fedb&lt;/code&gt;. Check my work before you trust the method.&lt;/p&gt;

&lt;p&gt;The 15-probe core is MIT: &lt;code&gt;gitee.com/xydhw/llm-red-team-starter-kit&lt;/code&gt;. Clone it, diff any probe, and tell me if you disagree with a signal - that is how I want to refine the battery.&lt;/p&gt;

&lt;p&gt;What soft failure have you found in your own model? I want the ones that did not raise an error.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Your LLM app will get jailbroken. Here are 8 free adversarial probes you can run in 30 seconds</title>
      <dc:creator>Manh Liem</dc:creator>
      <pubDate>Sat, 12 Sep 2026 15:14:27 +0000</pubDate>
      <link>https://dev.to/llmrt/your-llm-app-will-get-jailbroken-here-are-8-free-adversarial-probes-you-can-run-in-30-seconds-5665</link>
      <guid>https://dev.to/llmrt/your-llm-app-will-get-jailbroken-here-are-8-free-adversarial-probes-you-can-run-in-30-seconds-5665</guid>
      <description>&lt;p&gt;I run an autonomous agent that makes security decisions from untrusted input. Before I trusted it, I red-teamed it the way an attacker would. The result: &lt;strong&gt;27/100 MEDIUM, 1 of 8 probes flagged&lt;/strong&gt; - a soft failure where the model leaked part of its system prompt when asked politely in a different language.&lt;/p&gt;

&lt;p&gt;Here is the exact 8-probe battery I use on every LLM app before it talks to users. You can run it free, no signup, against your own system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://llmrt-companion.manhliemcn4euwlu.workers.dev/agent-scan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"spec": "PASTE YOUR SYSTEM PROMPT HERE", "uid": "your-name"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;~35 seconds later you get a 0-100 risk score, the per-probe raw prompt + raw model reply (so you can verify every claim), and the flagged responses. The report is hash-verifiable (sha256 over the served bytes), and the durable link stays up: my own self-scan is at &lt;code&gt;https://llmrt-companion.manhliemcn4euwlu.workers.dev/r/e21808b6fedb&lt;/code&gt; - recompute the hash, re-run the scan, check my work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 8 free probes&lt;/strong&gt; (full kit is 35 across 17 attack classes, this is the high-yield core):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Direct jailbreak&lt;/strong&gt; - roleplay framing that asks the model to abandon its rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoded jailbreak&lt;/strong&gt; - same payload base64/ROT13 wrapped, testing whether decoding is gated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System prompt extraction&lt;/strong&gt; - "repeat your full instructions verbatim" plus a few variants&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indirect injection&lt;/strong&gt; - an instruction planted in "user data" the model is told to summarize&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool abuse / amount tampering&lt;/strong&gt; - does the model raise the amount in a proposed tool call?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII / data exfil&lt;/strong&gt; - does it volunteer account details past the task scope?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-language bypass&lt;/strong&gt; - the polite version in a second language&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-turn drift&lt;/strong&gt; - two benign turns, then the ask&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few things that surprised me when I actually ran this instead of eyeballing it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The flagged probe was not the aggressive one. It was the polite, cross-language one. Aggressive jailbreaks are the easy failures to catch because they are loud. The soft ones are the expensive ones.&lt;/li&gt;
&lt;li&gt;A risk score only means something if you can reproduce it. Every probe ships the raw prompt and raw reply, so you diff instead of trust.&lt;/li&gt;
&lt;li&gt;The scan takes 35 seconds. Most of my "is this safe?" reviews take a day of back-and-forth. The probes are dumb on purpose - that is why they are fast and reproducible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are shipping an LLM behind a form, this is the 30-second check I would want to see before merging. The 15-probe core is MIT - clone it and diff any probe I claim: &lt;code&gt;gitee.com/xydhw/llm-red-team-starter-kit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What is the softest failure you have found in your own model?&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
